mirror of
https://github.com/esengine/DeepSeek-Reasonix.git
synced 2026-11-07 02:36:01 -10:00
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:
1 parent
b8b7ac87f9
commit
33f4e9f642
8 files changed
+535
-2
No files matched your search
@@ -421,6 +421,56 @@ jobs:
|
||||
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)
|
||||
if: env.RUN_STEPS == 'true' && runner.os == 'Windows' && github.event_name == 'pull_request'
|
||||
timeout-minutes: 5
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -28,6 +28,9 @@
|
||||
// commondir file or a damaged HEAD written afterwards cannot substitute
|
||||
// another repository's config. StageAll stages without entering a nested
|
||||
// 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
|
||||
// callers add --no-checkout and populate through gitcmd in the new worktree.
|
||||
|
||||
@@ -188,7 +188,7 @@ func Detached(cmd *exec.Cmd) (cleanup func(), err error) {
|
||||
}
|
||||
env = slices.DeleteFunc(slices.Clone(env), func(kv string) bool {
|
||||
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.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":
|
||||
return true
|
||||
}
|
||||
return false
|
||||
return storageOverride(strings.ToUpper(key))
|
||||
})
|
||||
return append(env,
|
||||
// Read-only probes must not take the index lock.
|
||||
@@ -243,3 +243,10 @@ func Env() []string {
|
||||
"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.
|
||||
Reference in new issue
Block a user