From c682f9006cf4947c469fe3b9513b3b4a39f1ab49 Mon Sep 17 00:00:00 2001 From: Nicolas De Loof Date: Wed, 16 Sep 2026 08:40:19 +0200 Subject: [PATCH 1/3] e2e: scenario DSL learns remote compose sources FromRemote switches a scenario's -f to a reference handled by compose's remote loaders (git URL, oci:// artifact) with optional root flags such as --insecure-registry, while the anchored testdata copy keeps serving as the local content the remote is built from. ContainerEnv joins the check vocabulary: the container-config environment is the natural observable that a remote project's bundled files (env_file) were consumed, whatever the source of the model. Signed-off-by: Nicolas De Loof --- pkg/e2e/SCENARIO.md | 6 ++++++ pkg/e2e/checks.go | 31 +++++++++++++++++++++++++++++++ pkg/e2e/scenario.go | 24 ++++++++++++++++++++++-- 3 files changed, 59 insertions(+), 2 deletions(-) diff --git a/pkg/e2e/SCENARIO.md b/pkg/e2e/SCENARIO.md index 18676b9610..46622cb041 100644 --- a/pkg/e2e/SCENARIO.md +++ b/pkg/e2e/SCENARIO.md @@ -56,6 +56,12 @@ Rules: `testdata` directory no test owns. Interpolate runtime values via `Env`. - **Regression tests link the issue** in a comment above the test, with a sentence on the failure mode being locked. +- **Remote sources**: a scenario consuming its project through a remote + loader (a git URL, an `oci://` artifact) still anchors the content in + `testdata//`, builds the remote from that copy — `serveGitRepo` + serves it over in-process smart HTTP, a `publish` step pushes it to a local + registry — then switches with `FromRemote(source, rootFlags...)`. Steps + before the switch run against the local copy (e.g. the `publish` itself). ## Checks: observe real state diff --git a/pkg/e2e/checks.go b/pkg/e2e/checks.go index 43366c3b28..65a9646cc2 100644 --- a/pkg/e2e/checks.go +++ b/pkg/e2e/checks.go @@ -23,6 +23,7 @@ package e2e // test-specific logic), and are named after the observable they assert. import ( + "encoding/json" "errors" "fmt" "os" @@ -507,6 +508,36 @@ func FileAbsent(path string) Check { } } +// ContainerEnv expects every container of the service to carry the given +// environment variable with the exact value, as recorded in the container +// config — the observable effect of `environment`, `env_file` or provider +// injection, whatever the source of the model. +func ContainerEnv(service, name, value string) Check { + return Check{ + name: fmt.Sprintf("service %q containers have env %s=%s", service, name, value), + fn: func(ctx *CheckContext) error { + containers := ctx.curr.service(service) + if len(containers) == 0 { + return errors.New("service has no container") + } + for _, c := range containers { + res := icmd.RunCmd(ctx.scenario.cli.NewDockerCmd(ctx.scenario.t, "inspect", "--format", "{{json .Config.Env}}", c.ID)) + if res.ExitCode != 0 { + return fmt.Errorf("inspect failed: %s", res.Combined()) + } + var env []string + if err := json.Unmarshal([]byte(strings.TrimSpace(res.Stdout())), &env); err != nil { + return err + } + if !slices.Contains(env, name+"="+value) { + return fmt.Errorf("not in container %s environment: %v", c.Name, env) + } + } + return nil + }, + } +} + // LabelSet expects every container of the service to carry a non-empty label. func LabelSet(service, key string) Check { return Check{ diff --git a/pkg/e2e/scenario.go b/pkg/e2e/scenario.go index 680f4bf079..3d43752c7e 100644 --- a/pkg/e2e/scenario.go +++ b/pkg/e2e/scenario.go @@ -45,6 +45,8 @@ type Scenario struct { intent string project string file string + remote string + rootArgs []string env []string parallel bool start time.Time @@ -253,6 +255,19 @@ func (s *Scenario) Env(kv ...string) *Scenario { return s } +// FromRemote switches the scenario's compose source to a remote reference +// handled by compose's remote loaders — a git URL or an oci:// artifact — +// with optional extra root flags (e.g. --insecure-registry) inserted before +// the subcommand on every subsequent step. Steps executed before the switch +// still run against the anchored testdata copy, which remains available +// through Dir() as the local content the remote was built from (a repository +// to commit, a project to publish). +func (s *Scenario) FromRemote(source string, rootFlags ...string) *Scenario { + s.remote = source + s.rootArgs = rootFlags + return s +} + // Requires skips the scenario unless every requirement is met by the target // environment. func (s *Scenario) Requires(reqs ...Requirement) *Scenario { @@ -324,10 +339,15 @@ func (s *Scenario) command(action Action) icmd.Cmd { switch action.kind { case kindCompose: args := []string{} - if s.file != "" { - args = append(args, "-f", s.file) + file := s.file + if s.remote != "" { + file = s.remote + } + if file != "" { + args = append(args, "-f", file) } args = append(args, "--project-name", s.project) + args = append(args, s.rootArgs...) args = append(args, action.args...) cmd = s.cli.NewDockerComposeCmd(s.t, args...) case kindDocker: From 5109a754e8ef43e6cef6221eb9285e7f9a4a10a4 Mon Sep 17 00:00:00 2001 From: Nicolas De Loof Date: Wed, 16 Sep 2026 08:41:02 +0200 Subject: [PATCH 2/3] e2e: cover git and OCI remote stacks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The remote loaders had almost no end-to-end coverage: one test consumed an OCI artifact (config only, plus an up declined at the prompt) and nothing exercised the git loader at all. Git scenarios run against a throwaway repository served over the smart HTTP protocol by an in-process server — git http-backend as a CGI per httptest request: no daemon, no container, no fixed port. Smart HTTP is a hard requirement: the loader ls-remotes the ref then shallow-fetches the raw commit, which the dumb protocol cannot serve (no shallow capability) and which needs uploadpack.allowAnySHA1InWant. Covered: deploying from the default branch with repository-relative files, selecting a #branch, selecting a #ref:subdir project. OCI scenarios publish the fixture to a throwaway local registry (the TestPublish pattern) then deploy from oci://: a full up consuming the bundled env-file layer, and tag selection between two published revisions. Signed-off-by: Nicolas De Loof --- pkg/e2e/remote_git_test.go | 142 ++++++++++++++++++ pkg/e2e/remote_oci_test.go | 93 ++++++++++++ .../TestGitRemoteBranchSelection/app.env | 1 + .../TestGitRemoteBranchSelection/compose.yaml | 7 + pkg/e2e/testdata/TestGitRemoteSubdir/app.env | 1 + .../TestGitRemoteSubdir/apps/web/app.env | 1 + .../TestGitRemoteSubdir/apps/web/compose.yaml | 7 + .../testdata/TestGitRemoteSubdir/compose.yaml | 7 + pkg/e2e/testdata/TestGitRemoteUp/app.env | 1 + pkg/e2e/testdata/TestGitRemoteUp/compose.yaml | 7 + .../TestOciRemoteTagSelection/app.env | 1 + .../TestOciRemoteTagSelection/compose.yaml | 7 + pkg/e2e/testdata/TestOciRemoteUp/app.env | 1 + pkg/e2e/testdata/TestOciRemoteUp/compose.yaml | 7 + 14 files changed, 283 insertions(+) create mode 100644 pkg/e2e/remote_git_test.go create mode 100644 pkg/e2e/remote_oci_test.go create mode 100644 pkg/e2e/testdata/TestGitRemoteBranchSelection/app.env create mode 100644 pkg/e2e/testdata/TestGitRemoteBranchSelection/compose.yaml create mode 100644 pkg/e2e/testdata/TestGitRemoteSubdir/app.env create mode 100644 pkg/e2e/testdata/TestGitRemoteSubdir/apps/web/app.env create mode 100644 pkg/e2e/testdata/TestGitRemoteSubdir/apps/web/compose.yaml create mode 100644 pkg/e2e/testdata/TestGitRemoteSubdir/compose.yaml create mode 100644 pkg/e2e/testdata/TestGitRemoteUp/app.env create mode 100644 pkg/e2e/testdata/TestGitRemoteUp/compose.yaml create mode 100644 pkg/e2e/testdata/TestOciRemoteTagSelection/app.env create mode 100644 pkg/e2e/testdata/TestOciRemoteTagSelection/compose.yaml create mode 100644 pkg/e2e/testdata/TestOciRemoteUp/app.env create mode 100644 pkg/e2e/testdata/TestOciRemoteUp/compose.yaml diff --git a/pkg/e2e/remote_git_test.go b/pkg/e2e/remote_git_test.go new file mode 100644 index 0000000000..0c728baffc --- /dev/null +++ b/pkg/e2e/remote_git_test.go @@ -0,0 +1,142 @@ +//go:build e2e + +/* +Copyright 2026 Docker Compose CLI authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +package e2e + +import ( + "net/http/cgi" + "net/http/httptest" + "os" + "os/exec" + "path/filepath" + "strings" + "testing" + "time" +) + +// gitRepo is a throwaway git repository served over the smart HTTP protocol +// by an in-process server: `git http-backend` run as a CGI per request, on a +// random port picked by httptest. No daemon process, no container — hermetic +// and port-collision free. The smart protocol is required: compose's git +// loader resolves the ref with ls-remote then shallow-fetches the raw commit, +// which the dumb protocol supports neither of (no shallow capability), and +// fetching a commit by hash needs uploadpack.allowAnySHA1InWant. +type gitRepo struct { + t *testing.T + work string // working tree the fixture content is committed from + bare string // bare repository the server exposes + URL string // smart-HTTP URL of the repository (…/repo.git) +} + +// serveGitRepo commits the content of dir on a `main` branch and serves the +// resulting repository over smart HTTP for the lifetime of the test. +func serveGitRepo(t *testing.T, dir string) *gitRepo { + t.Helper() + gitPath, err := exec.LookPath("git") + if err != nil { + t.Skip("git is not available in PATH") + } + root := t.TempDir() + r := &gitRepo{t: t, work: dir, bare: filepath.Join(root, "repo.git")} + r.git(dir, "init", "-q", "-b", "main", ".") + r.git(dir, "add", "-A") + r.git(dir, "commit", "-q", "-m", "e2e fixture") + r.git(dir, "clone", "-q", "--bare", ".", r.bare) + r.git(r.bare, "config", "uploadpack.allowAnySHA1InWant", "true") + + server := httptest.NewServer(&cgi.Handler{ + Path: gitPath, + Args: []string{"http-backend"}, + Env: []string{"GIT_PROJECT_ROOT=" + root, "GIT_HTTP_EXPORT_ALL=1"}, + }) + t.Cleanup(server.Close) + r.URL = server.URL + "/repo.git" + return r +} + +// Branch publishes a variant of the fixture under a new branch: mutate edits +// the working tree, and the resulting commit is pushed to the served +// repository. +func (r *gitRepo) Branch(name string, mutate func(dir string)) { + r.t.Helper() + r.git(r.work, "checkout", "-q", "-b", name) + mutate(r.work) + r.git(r.work, "add", "-A") + r.git(r.work, "commit", "-q", "-m", "branch "+name) + r.git(r.work, "push", "-q", r.bare, name) +} + +// git runs a git command against a fully isolated configuration: no user or +// system gitconfig (so a developer's signing or hook setup cannot leak into +// the fixture) and a fixed identity. +func (r *gitRepo) git(dir string, args ...string) { + r.t.Helper() + cmd := exec.Command("git", args...) + cmd.Dir = dir + cmd.Env = append(os.Environ(), + "GIT_CONFIG_GLOBAL=/dev/null", + "GIT_CONFIG_SYSTEM=/dev/null", + "GIT_AUTHOR_NAME=compose-e2e", + "GIT_AUTHOR_EMAIL=e2e@compose.invalid", + "GIT_COMMITTER_NAME=compose-e2e", + "GIT_COMMITTER_EMAIL=e2e@compose.invalid", + ) + out, err := cmd.CombinedOutput() + if err != nil { + r.t.Fatalf("git %s: %v\n%s", strings.Join(args, " "), err, out) + } +} + +func TestGitRemoteUp(t *testing.T) { + s := NewScenario(t, "up on a git remote must deploy the project, resolving its files against the fetched copy") + repo := serveGitRepo(t, s.Dir()) + s.Env("XDG_CACHE_HOME=" + t.TempDir()) + s.FromRemote(repo.URL) + s.Step("up fetches the repository and starts the service", + ComposeCmd("up", "-d", "--wait", "--yes").Within(60*time.Second), + ServiceState("app", "running"), + // the env_file exists only inside the repository: its effect proves + // the relative reference was resolved against the fetched copy + ContainerEnv("app", "FLAVOR", "main")) +} + +func TestGitRemoteBranchSelection(t *testing.T) { + s := NewScenario(t, "a #branch fragment on a git remote must deploy that branch's revision of the project") + repo := serveGitRepo(t, s.Dir()) + repo.Branch("feature", func(dir string) { + if err := os.WriteFile(filepath.Join(dir, "app.env"), []byte("FLAVOR=feature\n"), 0o644); err != nil { + t.Fatal(err) + } + }) + s.Env("XDG_CACHE_HOME=" + t.TempDir()) + s.FromRemote(repo.URL + "#feature") + s.Step("up deploys the feature branch, not the default one", + ComposeCmd("up", "-d", "--wait", "--yes").Within(60*time.Second), + ServiceState("app", "running"), + ContainerEnv("app", "FLAVOR", "feature")) +} + +func TestGitRemoteSubdir(t *testing.T) { + s := NewScenario(t, "a #ref:subdir fragment must load the project from the repository subdirectory, not its root") + repo := serveGitRepo(t, s.Dir()) + s.Env("XDG_CACHE_HOME=" + t.TempDir()) + s.FromRemote(repo.URL + "#main:apps/web") + s.Step("up deploys the subdirectory project, ignoring the decoy at the repository root", + ComposeCmd("up", "-d", "--wait", "--yes").Within(60*time.Second), + ServiceState("app", "running"), + ContainerEnv("app", "FLAVOR", "web")) +} diff --git a/pkg/e2e/remote_oci_test.go b/pkg/e2e/remote_oci_test.go new file mode 100644 index 0000000000..f06c3a2763 --- /dev/null +++ b/pkg/e2e/remote_oci_test.go @@ -0,0 +1,93 @@ +//go:build e2e + +/* +Copyright 2026 Docker Compose CLI authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +package e2e + +import ( + "net/http" + "os" + "path/filepath" + "strings" + "testing" + "time" + + "gotest.tools/v3/poll" +) + +// startLocalRegistry runs a throwaway registry container on a random host +// port for the lifetime of the scenario and returns its host:port address. +func startLocalRegistry(t *testing.T, s *Scenario) string { + t.Helper() + c := s.CLI() + name := s.Project() + "-registry" + c.RunDockerCmd(t, "run", "--name", name, "-P", "-d", "registry:3") + s.Defer(DockerCmd("rm", "--force", name)) + port := c.RunDockerCmd(t, "inspect", "--format", `{{ (index (index .NetworkSettings.Ports "5000/tcp") 0).HostPort }}`, name).Stdout() + registry := "localhost:" + strings.TrimSpace(port) + + registryURL := "http://" + registry + "/v2/" + poll.WaitOn(t, func(l poll.LogT) poll.Result { + resp, err := http.Get(registryURL) //nolint:gosec,noctx + if err != nil { + return poll.Continue("registry not ready: %v", err) + } + _ = resp.Body.Close() + if resp.StatusCode < 500 { + return poll.Success() + } + return poll.Continue("registry not ready, status %d", resp.StatusCode) + }, poll.WithTimeout(10*time.Second), poll.WithDelay(500*time.Millisecond)) + return registry +} + +func TestOciRemoteUp(t *testing.T) { + s := NewScenario(t, "up on an oci:// artifact must deploy the published project, bundled env files included") + registry := startLocalRegistry(t, s) + ref := registry + "/remote-up:v1" + s.Env("XDG_CACHE_HOME=" + t.TempDir()) + s.Step("publish pushes the project and its env file to the registry", + ComposeCmd("publish", "--with-env", "--yes", "--insecure-registry", ref)) + s.FromRemote("oci://"+ref, "--insecure-registry", registry) + s.Step("up pulls the artifact and starts the service", + ComposeCmd("up", "-d", "--wait", "--yes").Within(60*time.Second), + ServiceState("app", "running"), + // the env file travels as an artifact layer: its effect proves the + // bundle was consumed whole, not just the compose.yaml + ContainerEnv("app", "FLAVOR", "published")) +} + +func TestOciRemoteTagSelection(t *testing.T) { + s := NewScenario(t, "the tag of an oci:// reference must select which published revision is deployed") + registry := startLocalRegistry(t, s) + refV1 := registry + "/remote-tags:v1" + refV2 := registry + "/remote-tags:v2" + s.Env("XDG_CACHE_HOME=" + t.TempDir()) + s.Step("publish the v1 revision", + ComposeCmd("publish", "--with-env", "--yes", "--insecure-registry", refV1)) + // fixture preparation for the second revision, like a git branch: the + // anchored copy is edited before publishing under the other tag + if err := os.WriteFile(filepath.Join(s.Dir(), "app.env"), []byte("FLAVOR=v2\n"), 0o644); err != nil { + t.Fatal(err) + } + s.Step("publish the v2 revision under another tag", + ComposeCmd("publish", "--with-env", "--yes", "--insecure-registry", refV2)) + s.FromRemote("oci://"+refV1, "--insecure-registry", registry) + s.Step("up on the v1 tag deploys the v1 revision, not the latest published one", + ComposeCmd("up", "-d", "--wait", "--yes").Within(60*time.Second), + ServiceState("app", "running"), + ContainerEnv("app", "FLAVOR", "v1")) +} diff --git a/pkg/e2e/testdata/TestGitRemoteBranchSelection/app.env b/pkg/e2e/testdata/TestGitRemoteBranchSelection/app.env new file mode 100644 index 0000000000..fbf5e9b6c2 --- /dev/null +++ b/pkg/e2e/testdata/TestGitRemoteBranchSelection/app.env @@ -0,0 +1 @@ +FLAVOR=main diff --git a/pkg/e2e/testdata/TestGitRemoteBranchSelection/compose.yaml b/pkg/e2e/testdata/TestGitRemoteBranchSelection/compose.yaml new file mode 100644 index 0000000000..ee475f168d --- /dev/null +++ b/pkg/e2e/testdata/TestGitRemoteBranchSelection/compose.yaml @@ -0,0 +1,7 @@ +services: + app: + image: alpine + init: true + command: sleep infinity + env_file: + - ./app.env diff --git a/pkg/e2e/testdata/TestGitRemoteSubdir/app.env b/pkg/e2e/testdata/TestGitRemoteSubdir/app.env new file mode 100644 index 0000000000..0f788a688f --- /dev/null +++ b/pkg/e2e/testdata/TestGitRemoteSubdir/app.env @@ -0,0 +1 @@ +FLAVOR=root diff --git a/pkg/e2e/testdata/TestGitRemoteSubdir/apps/web/app.env b/pkg/e2e/testdata/TestGitRemoteSubdir/apps/web/app.env new file mode 100644 index 0000000000..b2d0ed1c6d --- /dev/null +++ b/pkg/e2e/testdata/TestGitRemoteSubdir/apps/web/app.env @@ -0,0 +1 @@ +FLAVOR=web diff --git a/pkg/e2e/testdata/TestGitRemoteSubdir/apps/web/compose.yaml b/pkg/e2e/testdata/TestGitRemoteSubdir/apps/web/compose.yaml new file mode 100644 index 0000000000..ee475f168d --- /dev/null +++ b/pkg/e2e/testdata/TestGitRemoteSubdir/apps/web/compose.yaml @@ -0,0 +1,7 @@ +services: + app: + image: alpine + init: true + command: sleep infinity + env_file: + - ./app.env diff --git a/pkg/e2e/testdata/TestGitRemoteSubdir/compose.yaml b/pkg/e2e/testdata/TestGitRemoteSubdir/compose.yaml new file mode 100644 index 0000000000..ee475f168d --- /dev/null +++ b/pkg/e2e/testdata/TestGitRemoteSubdir/compose.yaml @@ -0,0 +1,7 @@ +services: + app: + image: alpine + init: true + command: sleep infinity + env_file: + - ./app.env diff --git a/pkg/e2e/testdata/TestGitRemoteUp/app.env b/pkg/e2e/testdata/TestGitRemoteUp/app.env new file mode 100644 index 0000000000..fbf5e9b6c2 --- /dev/null +++ b/pkg/e2e/testdata/TestGitRemoteUp/app.env @@ -0,0 +1 @@ +FLAVOR=main diff --git a/pkg/e2e/testdata/TestGitRemoteUp/compose.yaml b/pkg/e2e/testdata/TestGitRemoteUp/compose.yaml new file mode 100644 index 0000000000..ee475f168d --- /dev/null +++ b/pkg/e2e/testdata/TestGitRemoteUp/compose.yaml @@ -0,0 +1,7 @@ +services: + app: + image: alpine + init: true + command: sleep infinity + env_file: + - ./app.env diff --git a/pkg/e2e/testdata/TestOciRemoteTagSelection/app.env b/pkg/e2e/testdata/TestOciRemoteTagSelection/app.env new file mode 100644 index 0000000000..5ff2abc64f --- /dev/null +++ b/pkg/e2e/testdata/TestOciRemoteTagSelection/app.env @@ -0,0 +1 @@ +FLAVOR=v1 diff --git a/pkg/e2e/testdata/TestOciRemoteTagSelection/compose.yaml b/pkg/e2e/testdata/TestOciRemoteTagSelection/compose.yaml new file mode 100644 index 0000000000..ee475f168d --- /dev/null +++ b/pkg/e2e/testdata/TestOciRemoteTagSelection/compose.yaml @@ -0,0 +1,7 @@ +services: + app: + image: alpine + init: true + command: sleep infinity + env_file: + - ./app.env diff --git a/pkg/e2e/testdata/TestOciRemoteUp/app.env b/pkg/e2e/testdata/TestOciRemoteUp/app.env new file mode 100644 index 0000000000..adfb0cb0a1 --- /dev/null +++ b/pkg/e2e/testdata/TestOciRemoteUp/app.env @@ -0,0 +1 @@ +FLAVOR=published diff --git a/pkg/e2e/testdata/TestOciRemoteUp/compose.yaml b/pkg/e2e/testdata/TestOciRemoteUp/compose.yaml new file mode 100644 index 0000000000..ee475f168d --- /dev/null +++ b/pkg/e2e/testdata/TestOciRemoteUp/compose.yaml @@ -0,0 +1,7 @@ +services: + app: + image: alpine + init: true + command: sleep infinity + env_file: + - ./app.env From d21fd0df65e13bc1a4e099806899f1738fa7fa62 Mon Sep 17 00:00:00 2001 From: Nicolas De Loof Date: Wed, 16 Sep 2026 09:01:46 +0200 Subject: [PATCH 3/3] e2e: version-proof git fixture init, Branch cuts from main git init -b requires git >= 2.28; init followed by symbolic-ref names the initial branch on any version. Branch() now returns the working tree to main so successive calls cut from the same base instead of stacking on the previous branch. Signed-off-by: Nicolas De Loof --- pkg/e2e/remote_git_test.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/pkg/e2e/remote_git_test.go b/pkg/e2e/remote_git_test.go index 0c728baffc..8057c294b5 100644 --- a/pkg/e2e/remote_git_test.go +++ b/pkg/e2e/remote_git_test.go @@ -52,7 +52,10 @@ func serveGitRepo(t *testing.T, dir string) *gitRepo { } root := t.TempDir() r := &gitRepo{t: t, work: dir, bare: filepath.Join(root, "repo.git")} - r.git(dir, "init", "-q", "-b", "main", ".") + // init then set HEAD explicitly: `git init -b` requires git >= 2.28, + // symbolic-ref names the initial branch on any version + r.git(dir, "init", "-q", ".") + r.git(dir, "symbolic-ref", "HEAD", "refs/heads/main") r.git(dir, "add", "-A") r.git(dir, "commit", "-q", "-m", "e2e fixture") r.git(dir, "clone", "-q", "--bare", ".", r.bare) @@ -78,6 +81,8 @@ func (r *gitRepo) Branch(name string, mutate func(dir string)) { r.git(r.work, "add", "-A") r.git(r.work, "commit", "-q", "-m", "branch "+name) r.git(r.work, "push", "-q", r.bare, name) + // return to main so each Branch call cuts from the same base + r.git(r.work, "checkout", "-q", "main") } // git runs a git command against a fully isolated configuration: no user or