fix(gitcmd): bind host storage to the resolved repository

Problem: ambient Git index/object variables can redirect host worktree preparation and object access.

Root cause: pinned Git/common/worktree directories do not override GIT_INDEX_FILE, GIT_OBJECT_DIRECTORY or GIT_ALTERNATE_OBJECT_DIRECTORIES. The shared constructors retained them.

Fix: remove those three ambient overrides in Env and Detached. Keep deliberate host private-index/object overrides and repository-declared alternates. Cover actual delivery/candidate preparation and a Boot isolated-write/apply flow.

Verification: all 239 root packages pass across disjoint required/rest commands; complete gitcmd/worktree/isolation race 306 passes; Boot race four passes. Exact preceding owner compiles and fails 18 regression events while seven controls pass. Full lint, vet and make check pass with byte-identical baseline. Windows/Linux builds compile; local literal Windows inventory verifies 25 required events and three package terminals, not native execution.
This commit is contained in:
SivanCola committed 2026-10-27 05:38:13 +08:00
1 parent b8b7ac87f9
commit 33f4e9f642
8 files changed
+535 -2

No files matched your search

+50
View File
@@ -421,6 +421,56 @@ jobs:
retention-days: 7 retention-days: 7
- name: test (Windows Git storage environment)
if: env.RUN_STEPS == 'true' && runner.os == 'Windows' && github.event_name == 'pull_request'
timeout-minutes: 5
shell: bash
run: |
set -o pipefail
go test -count=1 -timeout=3m -json -run '^Test(HostStorageEnvironment|HostObjectStorage|HostExplicitStorageControls|DetachedStorageEnvironment|WorktreePreparationOwnsItsIndex|EffectIsolatedTaskOwnsGitStorage)$' ./internal/platform/gitcmd ./internal/platform/worktree ./internal/assembly/boot | tee "$RUNNER_TEMP/git-storage-environment.jsonl"
jq -se '
[.[] | select(.Test == null and .Action == "pass") | .Package] as $packages |
[.[] | select(.Test != null and .Action == "pass") | .Test] as $tests |
all(.[]; .Action != "skip" and .Action != "fail") and
(["reasonix/internal/platform/gitcmd", "reasonix/internal/platform/worktree", "reasonix/internal/assembly/boot"] - $packages | length) == 0 and
([
"TestDetachedStorageEnvironment",
"TestDetachedStorageEnvironment/explicit_env",
"TestDetachedStorageEnvironment/inherited_env",
"TestEffectIsolatedTaskOwnsGitStorage",
"TestEffectIsolatedTaskOwnsGitStorage/ambient_existing",
"TestEffectIsolatedTaskOwnsGitStorage/ambient_missing",
"TestEffectIsolatedTaskOwnsGitStorage/control",
"TestHostExplicitStorageControls",
"TestHostExplicitStorageControls/explicit_object_directory",
"TestHostExplicitStorageControls/private_index",
"TestHostExplicitStorageControls/repository_declared_alternate",
"TestHostObjectStorage",
"TestHostObjectStorage/ambient_alternate_not_read",
"TestHostObjectStorage/writes_stay_in_repository",
"TestHostStorageEnvironment",
"TestHostStorageEnvironment/GIT_ALTERNATE_OBJECT_DIRECTORIES",
"TestHostStorageEnvironment/GIT_INDEX_FILE",
"TestHostStorageEnvironment/GIT_OBJECT_DIRECTORY",
"TestWorktreePreparationOwnsItsIndex",
"TestWorktreePreparationOwnsItsIndex/candidate_ambient_existing",
"TestWorktreePreparationOwnsItsIndex/candidate_ambient_missing",
"TestWorktreePreparationOwnsItsIndex/candidate_control",
"TestWorktreePreparationOwnsItsIndex/delivery_ambient_existing",
"TestWorktreePreparationOwnsItsIndex/delivery_ambient_missing",
"TestWorktreePreparationOwnsItsIndex/delivery_control"
] - $tests | length) == 0
' "$RUNNER_TEMP/git-storage-environment.jsonl"
- name: upload Windows Git storage environment inventory
if: always() && env.RUN_STEPS == 'true' && runner.os == 'Windows' && github.event_name == 'pull_request'
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7
with:
name: git-storage-environment-windows-${{ github.run_attempt }}
path: ${{ runner.temp }}/git-storage-environment.jsonl
if-no-files-found: ignore
retention-days: 7
- name: test (Windows Anthropic interrupted usage) - name: test (Windows Anthropic interrupted usage)
if: env.RUN_STEPS == 'true' && runner.os == 'Windows' && github.event_name == 'pull_request' if: env.RUN_STEPS == 'true' && runner.os == 'Windows' && github.event_name == 'pull_request'
timeout-minutes: 5 timeout-minutes: 5
+83
View File
@@ -0,0 +1,83 @@
---
owner: @SivanCola
backup: @esengine
status: active
reviewed: 2026-10-26
---
# Host Git storage environment / 宿主 Git 存储环境
## Behavior / 行为
Host Git commands now remove ambient `GIT_INDEX_FILE`, `GIT_OBJECT_DIRECTORY`
and `GIT_ALTERNATE_OBJECT_DIRECTORIES` before repository discovery, driver
inspection and execution. Detached URL commands remove them too, whether their
environment was explicit or inherited.
宿主 Git 命令在仓库发现、驱动检查和执行前移除上述三个继承变量。按 URL 运行的
Detached 命令也移除它们,包括显式环境和继承环境两种入口。
Pinning a Git directory and common directory alone does not choose its index or
object storage when these variables override them. See the official
[Git repository environment contract](https://git-scm.com/docs/git#_the_git_repository).
An inherited index could be overwritten while preparing another worktree;
objects could be written outside the resolved repository or found only through
an ambient alternate directory.
仅固定 Git 目录和 common 目录,不能覆盖这些变量指定的索引和对象存储。旧实现
创建另一个 worktree 时可改写外部索引;对象也可写入其他目录,或从继承的备用目录
读取。这里修复宿主命令的环境准备,不把 cwd 或独立索引当作完整隔离。
## Authority and compatibility / 权限与兼容
| Boundary | Contract / 约定 |
| --- | --- |
| Ambient storage variables | Removed from host Git constructors and Detached / 两个共享入口均移除 |
| Explicit host override | Trusted callers can append a private index or object directory after construction / 可信宿主仍可在构造后显式指定 |
| Repository alternates | Existing objects/info/alternates remains effective / 仓库声明的备用对象库仍有效 |
| User shell commands | Their environment is unchanged / 不改用户 shell 环境 |
| Repository selection | Existing Open and Repo identity rules remain / 沿用仓库解析与固定身份 |
| Credentials and transport | Existing secret filtering, SSH and network policy remain / 沿用密钥过滤、SSH 和网络策略 |
Host commands previously relying on ambient storage relocation now use the
resolved repository's storage. There is no migration of an existing external
index or object database. Explicit private-index snapshot staging remains
available. Legitimate linked worktrees still share their repository's common
object database.
此前依赖继承变量重定向存储的宿主命令,现在使用已解析仓库的存储;不迁移或修复
已有外部索引和对象库。快照的显式私有索引仍有效;合法 linked worktree 仍共享
仓库 common 对象库。
Only three storage variables are covered. This is not a blanket Git environment
allowlist, a sandbox, or authority against trusted explicit overrides, arbitrary
shells, other writers, repository configuration or declared object alternates.
No API, config, wire, persisted format, permission or model prefix changes.
仅覆盖三个变量,不是完整 Git 环境白名单或沙箱,也不约束可信显式覆盖、任意
shell、其他写入者、仓库配置和仓库声明的备用对象库。不改 API、配置、传输、持久化
格式、权限或模型前缀。
## Evidence and remaining work / 证据与未完成项
Disposable real Git fixtures exercise object writes, ambient alternate reads,
private-index staging, explicit object directories and declared alternates.
Delivery and candidate preparation preserve an existing ambient index and do
not create an absent one.
Actual Boot delegates an isolated write and applies
it, checking source-index bytes and that same ambient-index boundary.
真实临时 Git 夹具覆盖对象写入、继承备用目录读取、私有索引、显式对象目录及仓库
备用对象库。交付和候选 worktree 准备保留外部索引且不新建缺失索引。真实 Boot
委派隔离写入并应用,核对源索引字节和相同外部索引边界。
These are constructed regressions, not an observed user incident. They do not
establish complete snapshots, actor CAS, hardlink protection, physical process
quiescence, durable publication, cleanup recovery or the complete #14025 plan.
Native execution and hosted results belong to each exact PR head separately.
这些是构造回归,不是已观察到的用户事故;不能证明完整快照、逐 actor CAS、硬链接
防护、物理进程静默、持久化发布、清理恢复或整个 #14025 方案完成。平台执行和 CI
必须按各 PR 的精确 head 单独核验。
@@ -0,0 +1,106 @@
package boot
import (
"bytes"
"context"
"os"
"os/exec"
"path/filepath"
"strings"
"testing"
"reasonix/internal/base/testenv"
"reasonix/internal/contract/config"
"reasonix/internal/contract/event"
"reasonix/internal/contract/provider"
"reasonix/internal/session/control"
)
func TestEffectIsolatedTaskOwnsGitStorage(t *testing.T) {
if _, err := exec.LookPath("git"); err != nil {
t.Skip("Git unavailable")
}
for _, mode := range []string{"ambient_existing", "ambient_missing", "control"} {
t.Run(mode, func(t *testing.T) {
home, root := statedBootHome(t), testenv.TempDir(t)
kind := "boot-git-storage-" + mode
rec := &isolationScriptProvider{dir: root, managed: filepath.Join(config.RootsForHome(home).DeliveryWorktreeDir(), "isolated")}
provider.Register(kind, func(provider.Config) (provider.Provider, error) { return rec, nil })
writeFile(t, root, "reasonix.toml", `
default_model = "test-model"
[agent]
system_prompt = "BASE"
worktree_isolation = true
[codegraph]
enabled = false
[[providers]]
name = "test-model"
kind = "`+kind+`"
model = "x"
`)
gitInDir(t, root, "init", "-q")
mirrorToUserConfigAt(t, home, root)
gitInDir(t, root, "add", "-A")
gitInDir(t, root, "commit", "-q", "-m", "fixture")
index := filepath.Join(root, ".git", "index")
before, err := os.ReadFile(index)
if err != nil {
t.Fatal(err)
}
ambient := filepath.Join(testenv.TempDir(t), "ambient-index")
var original []byte
if mode == "ambient_existing" {
writeFile(t, root, "ambient-only.txt", "staged in another index\n")
for _, args := range [][]string{{"read-tree", "HEAD"}, {"add", "ambient-only.txt"}} {
cmd := exec.Command("git", append([]string{"-C", root}, args...)...)
cmd.Env = append(os.Environ(), "GIT_INDEX_FILE="+ambient)
if out, err := cmd.CombinedOutput(); err != nil {
t.Fatalf("ambient fixture: %v %s", err, out)
}
}
if err := os.Remove(filepath.Join(root, "ambient-only.txt")); err != nil {
t.Fatal(err)
}
original, err = os.ReadFile(ambient)
if err != nil {
t.Fatal(err)
}
}
if mode != "control" {
t.Setenv("GIT_INDEX_FILE", ambient)
}
ctrl, err := Build(context.Background(), Options{Home: home, WorkspaceRoot: root, Sink: event.Discard})
if err != nil {
t.Fatal(err)
}
t.Cleanup(ctrl.Close)
ctrl.SetToolApprovalMode(control.ToolApprovalYolo)
if err := ctrl.Run(t.Context(), "make out.txt without touching my tree until I say"); err != nil {
t.Fatal(err)
}
ctrl.Close()
if mode == "ambient_existing" {
after, err := os.ReadFile(ambient)
if err != nil || !bytes.Equal(original, after) {
t.Fatal("isolated task rewrote the ambient index")
}
}
if mode == "ambient_missing" {
if _, err := os.Stat(ambient); !os.IsNotExist(err) {
t.Fatalf("isolated task created an ambient index: %v", err)
}
}
after, err := os.ReadFile(index)
if err != nil || !bytes.Equal(before, after) {
t.Fatal("isolated task changed the source index")
}
out, err := os.ReadFile(filepath.Join(root, "out.txt"))
if err != nil || strings.ReplaceAll(string(out), "\r\n", "\n") != "made in isolation\n" {
t.Fatalf("applied result = %q %v", out, err)
}
if rec.landedBeforeApp || rec.childWorktree == "" {
t.Fatal("task did not write in its observed worktree before apply")
}
})
}
}
+3
View File
@@ -28,6 +28,9 @@
// commondir file or a damaged HEAD written afterwards cannot substitute // commondir file or a damaged HEAD written afterwards cannot substitute
// another repository's config. StageAll stages without entering a nested // another repository's config. StageAll stages without entering a nested
// repository, and host-created worktrees are opened as they are added. // repository, and host-created worktrees are opened as they are added.
// Ambient index, object-directory and alternate-object-directory overrides are
// removed before discovery and execution. Trusted callers may explicitly add a
// private index afterward; repository-declared object alternates still apply.
// //
// A checkout during `worktree add` runs in a child past the driver listing, so // A checkout during `worktree add` runs in a child past the driver listing, so
// callers add --no-checkout and populate through gitcmd in the new worktree. // callers add --no-checkout and populate through gitcmd in the new worktree.
+9 -2
View File
@@ -188,7 +188,7 @@ func Detached(cmd *exec.Cmd) (cleanup func(), err error) {
} }
env = slices.DeleteFunc(slices.Clone(env), func(kv string) bool { env = slices.DeleteFunc(slices.Clone(env), func(kv string) bool {
name, _, _ := strings.Cut(strings.ToUpper(kv), "=") name, _, _ := strings.Cut(strings.ToUpper(kv), "=")
return name == "GIT_DIR" || name == "GIT_WORK_TREE" || name == "GIT_COMMON_DIR" || name == "GIT_CEILING_DIRECTORIES" return storageOverride(name) || name == "GIT_DIR" || name == "GIT_WORK_TREE" || name == "GIT_COMMON_DIR" || name == "GIT_CEILING_DIRECTORIES"
}) })
cmd.Dir = dir cmd.Dir = dir
cmd.Env = append(env, "GIT_CEILING_DIRECTORIES="+filepath.Dir(dir)) cmd.Env = append(env, "GIT_CEILING_DIRECTORIES="+filepath.Dir(dir))
@@ -230,7 +230,7 @@ func Env() []string {
case "GIT_LITERAL_PATHSPECS", "GIT_GLOB_PATHSPECS", "GIT_NOGLOB_PATHSPECS", "GIT_ICASE_PATHSPECS": case "GIT_LITERAL_PATHSPECS", "GIT_GLOB_PATHSPECS", "GIT_NOGLOB_PATHSPECS", "GIT_ICASE_PATHSPECS":
return true return true
} }
return false return storageOverride(strings.ToUpper(key))
}) })
return append(env, return append(env,
// Read-only probes must not take the index lock. // Read-only probes must not take the index lock.
@@ -243,3 +243,10 @@ func Env() []string {
"GIT_NO_LAZY_FETCH=1", "GIT_NO_LAZY_FETCH=1",
) )
} }
// storageOverride identifies ambient locations that override a Repo's pinned
// identity. Trusted host callers can still append an explicit private index or
// object directory after command construction.
func storageOverride(name string) bool {
return name == "GIT_INDEX_FILE" || name == "GIT_OBJECT_DIRECTORY" || name == "GIT_ALTERNATE_OBJECT_DIRECTORIES"
}
@@ -0,0 +1,188 @@
package gitcmd
import (
"bytes"
"os"
"os/exec"
"path/filepath"
"strings"
"testing"
"reasonix/internal/base/testenv"
)
func storageFixture(t *testing.T, text string) Repo {
t.Helper()
if _, err := exec.LookPath("git"); err != nil {
t.Skip("Git unavailable")
}
dir := testenv.TempDir(t)
if out, err := Command(t.Context(), dir, "init", "-q").CombinedOutput(); err != nil {
t.Fatalf("setup: %v %s", err, out)
}
if err := os.WriteFile(filepath.Join(dir, "file.txt"), []byte(text), 0o600); err != nil {
t.Fatal(err)
}
for _, args := range [][]string{{"add", "file.txt"}, {"commit", "-q", "-m", "fixture"}} {
if out, err := CommandWithConfig(t.Context(), dir, []string{"user.name=Fixture", "[email protected]"}, args...).CombinedOutput(); err != nil {
t.Fatalf("setup: %v %s", err, out)
}
}
repo, err := Open(t.Context(), dir)
if err != nil {
t.Fatal(err)
}
return repo
}
func TestHostStorageEnvironment(t *testing.T) {
for _, key := range []string{"GIT_INDEX_FILE", "GIT_OBJECT_DIRECTORY", "GIT_ALTERNATE_OBJECT_DIRECTORIES"} {
t.Run(key, func(t *testing.T) {
t.Setenv(key, filepath.Join(t.TempDir(), "ambient"))
for _, entry := range Env() {
name, _, _ := strings.Cut(entry, "=")
if strings.EqualFold(name, key) {
t.Fatalf("ambient %s survived", key)
}
}
})
}
}
func TestHostObjectStorage(t *testing.T) {
t.Run("writes_stay_in_repository", func(t *testing.T) {
repo := storageFixture(t, "source\n")
outside := testenv.TempDir(t)
t.Setenv("GIT_OBJECT_DIRECTORY", outside)
cmd := repo.Command(t.Context(), "hash-object", "-w", "--stdin")
cmd.Stdin = strings.NewReader("new host object\n")
out, err := cmd.Output()
if err != nil {
t.Fatal(err)
}
oid := strings.TrimSpace(string(out))
if _, err := os.Stat(filepath.Join(repo.CommonDir, "objects", oid[:2], oid[2:])); err != nil {
t.Fatalf("object not in resolved repository: %v", err)
}
if _, err := os.Stat(filepath.Join(outside, oid[:2], oid[2:])); !os.IsNotExist(err) {
t.Fatalf("ambient object directory was written: %v", err)
}
})
t.Run("ambient_alternate_not_read", func(t *testing.T) {
repo := storageFixture(t, "source\n")
foreign := storageFixture(t, "foreign object\n")
out, err := foreign.Command(t.Context(), "rev-parse", "HEAD:file.txt").Output()
if err != nil {
t.Fatal(err)
}
oid := strings.TrimSpace(string(out))
t.Setenv("GIT_ALTERNATE_OBJECT_DIRECTORIES", filepath.Join(foreign.CommonDir, "objects"))
if err := repo.Command(t.Context(), "cat-file", "-e", oid).Run(); err == nil {
t.Fatal("host read an object only ambient alternates supplied")
}
})
}
func TestHostExplicitStorageControls(t *testing.T) {
t.Run("private_index", func(t *testing.T) {
repo := storageFixture(t, "before\n")
index := filepath.Join(repo.GitDir, "index")
before, err := os.ReadFile(index)
if err != nil {
t.Fatal(err)
}
ambient := filepath.Join(t.TempDir(), "ambient-index")
if err := os.WriteFile(ambient, []byte("not an index"), 0o600); err != nil {
t.Fatal(err)
}
t.Setenv("GIT_INDEX_FILE", ambient)
if err := os.WriteFile(filepath.Join(repo.WorkTree, "file.txt"), []byte("after\n"), 0o600); err != nil {
t.Fatal(err)
}
private := filepath.Join(t.TempDir(), "private-index")
if err := repo.StageAll(t.Context(), "GIT_INDEX_FILE="+private); err != nil {
t.Fatal(err)
}
cmd := repo.Command(t.Context(), "show", ":file.txt")
cmd.Env = append(cmd.Env, "GIT_INDEX_FILE="+private)
out, err := cmd.Output()
if err != nil || string(out) != "after\n" {
t.Fatalf("explicit private index = %q %v", out, err)
}
after, err := os.ReadFile(index)
if err != nil || !bytes.Equal(before, after) {
t.Fatal("private staging touched the user's real index")
}
untouched, err := os.ReadFile(ambient)
if err != nil || string(untouched) != "not an index" {
t.Fatal("explicit staging changed ambient index")
}
})
t.Run("explicit_object_directory", func(t *testing.T) {
repo := storageFixture(t, "source\n")
outside := testenv.TempDir(t)
t.Setenv("GIT_OBJECT_DIRECTORY", filepath.Join(t.TempDir(), "missing"))
cmd := repo.Command(t.Context(), "hash-object", "-w", "--stdin")
cmd.Env = append(cmd.Env, "GIT_OBJECT_DIRECTORY="+outside)
cmd.Stdin = strings.NewReader("explicit host object\n")
out, err := cmd.Output()
if err != nil {
t.Fatal(err)
}
oid := strings.TrimSpace(string(out))
if _, err := os.Stat(filepath.Join(outside, oid[:2], oid[2:])); err != nil {
t.Fatal(err)
}
})
t.Run("repository_declared_alternate", func(t *testing.T) {
repo := storageFixture(t, "source\n")
foreign := storageFixture(t, "declared alternate\n")
out, err := foreign.Command(t.Context(), "rev-parse", "HEAD:file.txt").Output()
if err != nil {
t.Fatal(err)
}
path := filepath.Join(repo.CommonDir, "objects", "info", "alternates")
if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(path, []byte(filepath.ToSlash(filepath.Join(foreign.CommonDir, "objects"))+"\n"), 0o600); err != nil {
t.Fatal(err)
}
if err := repo.Command(t.Context(), "cat-file", "-e", strings.TrimSpace(string(out))).Run(); err != nil {
t.Fatalf("declared alternate disabled: %v", err)
}
})
}
func TestDetachedStorageEnvironment(t *testing.T) {
if _, err := exec.LookPath("git"); err != nil {
t.Skip("Git unavailable")
}
for _, inherited := range []bool{false, true} {
t.Run(map[bool]string{false: "explicit_env", true: "inherited_env"}[inherited], func(t *testing.T) {
cmd := exec.Command("git", "--version")
if inherited {
t.Setenv("GIT_INDEX_FILE", "ambient-index")
t.Setenv("GIT_OBJECT_DIRECTORY", "ambient-objects")
t.Setenv("GIT_ALTERNATE_OBJECT_DIRECTORIES", "ambient-alternates")
} else {
cmd.Env = append(os.Environ(), "GIT_INDEX_FILE=ambient-index", "GIT_OBJECT_DIRECTORY=ambient-objects", "GIT_ALTERNATE_OBJECT_DIRECTORIES=ambient-alternates")
}
cleanup, err := Detached(cmd)
if err != nil {
t.Fatal(err)
}
defer cleanup()
for _, entry := range cmd.Env {
name, _, _ := strings.Cut(entry, "=")
switch strings.ToUpper(name) {
case "GIT_INDEX_FILE", "GIT_OBJECT_DIRECTORY", "GIT_ALTERNATE_OBJECT_DIRECTORIES":
t.Fatalf("detached command retained %s", name)
}
}
if out, err := cmd.CombinedOutput(); err != nil {
t.Fatalf("detached git: %v %s", err, out)
}
})
}
}
@@ -0,0 +1,94 @@
package worktree
import (
"bytes"
"os"
"os/exec"
"path/filepath"
"testing"
"reasonix/internal/base/testenv"
)
func TestWorktreePreparationOwnsItsIndex(t *testing.T) {
for _, kind := range []string{"delivery", "candidate"} {
for _, mode := range []string{"ambient_existing", "ambient_missing", "control"} {
t.Run(kind+"_"+mode, func(t *testing.T) {
requireGit(t)
root := initRepo(t)
repo := opened(t, root)
snap, err := TakeSnapshot(t.Context(), repo)
if err != nil {
t.Fatal(err)
}
index := filepath.Join(repo.GitDir, "index")
before, err := os.ReadFile(index)
if err != nil {
t.Fatal(err)
}
ambient := filepath.Join(testenv.TempDir(t), "ambient-index")
var original []byte
if mode == "ambient_existing" {
writeRepoFile(t, root, "ambient-only.txt", "ambient staged content\n")
for _, args := range [][]string{{"read-tree", "HEAD"}, {"add", "ambient-only.txt"}} {
cmd := exec.Command("git", append([]string{"-C", root}, args...)...)
cmd.Env = append(os.Environ(), "GIT_INDEX_FILE="+ambient)
if out, err := cmd.CombinedOutput(); err != nil {
t.Fatalf("ambient fixture: %v %s", err, out)
}
}
if err := os.Remove(filepath.Join(root, "ambient-only.txt")); err != nil {
t.Fatal(err)
}
original, err = os.ReadFile(ambient)
if err != nil {
t.Fatal(err)
}
}
if mode != "control" {
t.Setenv("GIT_INDEX_FILE", ambient)
}
managed := testenv.TempDir(t)
var path string
if kind == "delivery" {
result, err := Create(t.Context(), repo, managed)
if err != nil {
t.Fatal(err)
}
path = result.WorktreeRoot
} else {
candidate, err := CreateCandidate(t.Context(), snap, managed)
if err != nil {
t.Fatal(err)
}
path = candidate.WorktreeRoot
}
if mode == "ambient_existing" {
after, err := os.ReadFile(ambient)
if err != nil || !bytes.Equal(original, after) {
t.Fatal("preparing another worktree rewrote the ambient index")
}
}
if mode == "ambient_missing" {
if _, err := os.Stat(ambient); !os.IsNotExist(err) {
t.Fatalf("preparation created an ambient index: %v", err)
}
}
after, err := os.ReadFile(index)
if err != nil || !bytes.Equal(before, after) {
t.Fatal("preparation changed the source index")
}
child := opened(t, path)
if !child.Valid() {
t.Fatal("prepared checkout has no own repository identity")
}
if _, err := os.Stat(filepath.Join(child.GitDir, "index")); err != nil {
t.Fatalf("prepared checkout has no own index: %v", err)
}
if got := readRepoFile(t, path, "README.md"); got != "base\n" {
t.Fatalf("checkout bytes=%q", got)
}
})
}
}
}
@@ -0,0 +1,2 @@
- 修复:宿主 Git 的 worktree 准备不再继承外部索引或对象存储变量,保留显式私有索引和仓库备用对象库。关联 #14025。
- Fixed: host Git worktree preparation drops ambient index and object storage overrides, preserving explicit private indexes and repository alternates. Refs #14025.