diff --git a/.githooks/pre-push b/.githooks/pre-push index 87c9870..4a8cd19 100755 --- a/.githooks/pre-push +++ b/.githooks/pre-push @@ -32,6 +32,22 @@ if [ -z "$WORKSPACE_ROOT" ]; then exit 0 fi +GIT_LOCAL_ENV_VARS=$(git rev-parse --local-env-vars) || { + echo "Error: Could not determine repository-local Git environment" + exit 1 +} +for git_env_var in $GIT_LOCAL_ENV_VARS; do + case "$git_env_var" in + *[!A-Z0-9_]*|'') + echo "Error: Refusing invalid Git environment variable name: $git_env_var" + exit 1 + ;; + *) + unset "$git_env_var" + ;; + esac +done + cd "$WORKSPACE_ROOT" echo "Running pre-push checks from $WORKSPACE_ROOT..." diff --git a/tests/context.bats b/tests/context.bats index bdf8b83..6f8ab17 100644 --- a/tests/context.bats +++ b/tests/context.bats @@ -1,8 +1,11 @@ #!/usr/bin/env bats +load "${BATS_TEST_DIRNAME}/helpers/git_environment.bash" + # Integration tests for `meta context` setup() { + clear_git_local_env META_BIN="$BATS_TEST_DIRNAME/../target/debug/meta" if [ ! -f "$META_BIN" ]; then diff --git a/tests/exec.bats b/tests/exec.bats index af6071b..8994038 100644 --- a/tests/exec.bats +++ b/tests/exec.bats @@ -1,9 +1,12 @@ #!/usr/bin/env bats +load "${BATS_TEST_DIRNAME}/helpers/git_environment.bash" + # Integration tests for `meta exec` and loop pass-through options # Tests: exec, --include, --exclude, --parallel, --dry-run, --tag setup() { + clear_git_local_env META_BIN="$BATS_TEST_DIRNAME/../target/debug/meta" META_GIT_BIN="$BATS_TEST_DIRNAME/../target/debug/meta-git" diff --git a/tests/git.bats b/tests/git.bats index 0b97575..1958a85 100644 --- a/tests/git.bats +++ b/tests/git.bats @@ -1,9 +1,12 @@ #!/usr/bin/env bats +load "${BATS_TEST_DIRNAME}/helpers/git_environment.bash" + # Integration tests for `meta git` commands # Tests use local git repos (no network IO required) setup() { + clear_git_local_env # Build binaries if not already built META_BIN="$BATS_TEST_DIRNAME/../target/debug/meta" META_GIT_BIN="$BATS_TEST_DIRNAME/../target/debug/meta-git" @@ -376,7 +379,7 @@ assert backend['dirty'] == True, f'backend should be dirty, got {backend}' @test "meta git clone from local path works" { # Create a "remote" bare repo that IS a meta repo REMOTE_DIR="$(mktemp -d)" - git -C "$REMOTE_DIR" init --bare --quiet + git init --bare --quiet "$REMOTE_DIR" git -C "$REMOTE_DIR" symbolic-ref HEAD refs/heads/main # Create a temp working copy to push initial meta content @@ -415,7 +418,7 @@ EOF # Create a "remote" bare repo with main as default branch REMOTE_DIR="$(mktemp -d)" - git -C "$REMOTE_DIR" init --bare --quiet + git init --bare --quiet "$REMOTE_DIR" git -C "$REMOTE_DIR" symbolic-ref HEAD refs/heads/main # Create a temp working copy to push initial content @@ -457,7 +460,7 @@ EOF # Create a "remote" bare repo with main as default branch REMOTE_DIR="$(mktemp -d)" - git -C "$REMOTE_DIR" init --bare --quiet + git init --bare --quiet "$REMOTE_DIR" git -C "$REMOTE_DIR" symbolic-ref HEAD refs/heads/main # Push minimal content @@ -489,7 +492,7 @@ EOF @test "meta git clone with --depth option" { # Create a "remote" bare repo with main as default branch REMOTE_DIR="$(mktemp -d)" - git -C "$REMOTE_DIR" init --bare --quiet + git init --bare --quiet "$REMOTE_DIR" git -C "$REMOTE_DIR" symbolic-ref HEAD refs/heads/main WORK_DIR="$(mktemp -d)" diff --git a/tests/help.bats b/tests/help.bats index c4ee0db..c0f989f 100644 --- a/tests/help.bats +++ b/tests/help.bats @@ -1,6 +1,9 @@ #!/usr/bin/env bats +load "${BATS_TEST_DIRNAME}/helpers/git_environment.bash" + setup() { + clear_git_local_env META_BIN="$BATS_TEST_DIRNAME/../target/debug/meta" META_GIT_BIN="$BATS_TEST_DIRNAME/../target/debug/meta-git" META_PROJECT_BIN="$BATS_TEST_DIRNAME/../target/debug/meta-project" diff --git a/tests/helpers/git_environment.bash b/tests/helpers/git_environment.bash new file mode 100644 index 0000000..edc1e80 --- /dev/null +++ b/tests/helpers/git_environment.bash @@ -0,0 +1,14 @@ +clear_git_local_env() { + local git_env_var git_local_env_vars + git_local_env_vars="$(git rev-parse --local-env-vars)" || { + echo "Error: Could not determine repository-local Git environment" >&2 + return 1 + } + while IFS= read -r git_env_var; do + if [[ ! "$git_env_var" =~ ^[A-Z0-9_]+$ ]]; then + echo "Error: Refusing invalid Git environment variable name: $git_env_var" >&2 + return 1 + fi + unset "$git_env_var" + done <<< "$git_local_env_vars" +} diff --git a/tests/hooks.bats b/tests/hooks.bats new file mode 100644 index 0000000..f3bbe30 --- /dev/null +++ b/tests/hooks.bats @@ -0,0 +1,75 @@ +#!/usr/bin/env bats + +load "${BATS_TEST_DIRNAME}/helpers/git_environment.bash" + +setup() { + clear_git_local_env + TEST_DIR="$(mktemp -d)" +} + +teardown() { + rm -rf "$TEST_DIR" +} + +@test "pre-push clears repository-local Git environment without mutating caller config" { + local workspace="$TEST_DIR/workspace" + local fake_bin="$TEST_DIR/bin" + local calls="$TEST_DIR/cargo-calls" + local config_before="$TEST_DIR/config-before" + mkdir -p "$workspace" "$fake_bin" + printf '[workspace]\nmembers = []\n' >"$workspace/Cargo.toml" + git -C "$workspace" init --quiet + cp "$workspace/.git/config" "$config_before" + + cat >"$fake_bin/cargo" <<'EOF' +#!/bin/bash +set -e +while IFS= read -r git_env_var; do + if [[ "$git_env_var" =~ ^[A-Z0-9_]+$ ]] && env | grep -q "^${git_env_var}="; then + echo "${git_env_var} leaked into cargo" >&2 + exit 97 + fi +done < <(git rev-parse --local-env-vars) +git rev-parse --show-toplevel +printf '%s\n' "$*" >>"$META_TEST_CARGO_CALLS" +EOF + chmod +x "$fake_bin/cargo" + + cd "$workspace" + run env \ + PATH="$fake_bin:$PATH" \ + META_TEST_CARGO_CALLS="$calls" \ + GIT_DIR="$workspace/.git" \ + GIT_WORK_TREE="$workspace" \ + GIT_INDEX_FILE="$workspace/.git/index" \ + sh "$BATS_TEST_DIRNAME/../.githooks/pre-push" + + [ "$status" -eq 0 ] + [[ "$output" == *"$workspace"* ]] + [ "$(wc -l <"$calls" | tr -d ' ')" -eq 3 ] + cmp -s "$config_before" "$workspace/.git/config" +} + +@test "Git test environment helper fails closed when enumeration fails" { + local fake_bin="$TEST_DIR/bin" + local real_git + real_git="$(command -v git)" + mkdir -p "$fake_bin" + + cat >"$fake_bin/git" <<'EOF' +#!/bin/bash +if [[ "$1" == "rev-parse" && "$2" == "--local-env-vars" ]]; then + exit 73 +fi +exec "$META_TEST_REAL_GIT" "$@" +EOF + chmod +x "$fake_bin/git" + + run env \ + PATH="$fake_bin:$PATH" \ + META_TEST_REAL_GIT="$real_git" \ + bash -c 'source "$1"; clear_git_local_env' _ "$BATS_TEST_DIRNAME/helpers/git_environment.bash" + + [ "$status" -eq 1 ] + [[ "$output" == *"Could not determine repository-local Git environment"* ]] +} diff --git a/tests/init.bats b/tests/init.bats index c75de3d..5da09c0 100644 --- a/tests/init.bats +++ b/tests/init.bats @@ -1,8 +1,11 @@ #!/usr/bin/env bats +load "${BATS_TEST_DIRNAME}/helpers/git_environment.bash" + # Integration tests for `meta init claude` setup() { + clear_git_local_env META_BIN="$BATS_TEST_DIRNAME/../target/debug/meta" if [ ! -f "$META_BIN" ]; then diff --git a/tests/plugin_install.bats b/tests/plugin_install.bats index 6c82190..157c284 100755 --- a/tests/plugin_install.bats +++ b/tests/plugin_install.bats @@ -1,8 +1,11 @@ #!/usr/bin/env bats +load "${BATS_TEST_DIRNAME}/helpers/git_environment.bash" + # Integration tests for `meta plugin install` setup() { + clear_git_local_env META_BIN="$BATS_TEST_DIRNAME/../target/debug/meta" if [ ! -f "$META_BIN" ]; then diff --git a/tests/project_list.bats b/tests/project_list.bats index 1c93e2e..895cc7a 100644 --- a/tests/project_list.bats +++ b/tests/project_list.bats @@ -1,8 +1,11 @@ #!/usr/bin/env bats +load "${BATS_TEST_DIRNAME}/helpers/git_environment.bash" + # Integration tests for `meta project list` / `meta project ls` setup() { + clear_git_local_env # Build binaries if not already built META_BIN="$BATS_TEST_DIRNAME/../target/debug/meta" META_PROJECT_BIN="$BATS_TEST_DIRNAME/../target/debug/meta-project" diff --git a/tests/worktree.bats b/tests/worktree.bats index c52273b..340d12b 100644 --- a/tests/worktree.bats +++ b/tests/worktree.bats @@ -1,9 +1,12 @@ #!/usr/bin/env bats +load "${BATS_TEST_DIRNAME}/helpers/git_environment.bash" + # Integration tests for `meta git worktree` subcommand # Tests: create, add, list, status, diff, exec, remove, configuration, edge cases setup() { + clear_git_local_env META_BIN="$BATS_TEST_DIRNAME/../target/debug/meta" META_GIT_BIN="$BATS_TEST_DIRNAME/../target/debug/meta-git" @@ -103,6 +106,18 @@ EOF [ "$BRANCH" = "fix/the-bug" ] } +@test "worktree create reports corrupt bare config with scoped repair" { + git -C backend config --local core.bare true + + run "$META_BIN" git worktree create corrupt-source --repo backend + + [ "$status" -ne 0 ] + [[ "$output" == *"core.bare=true"* ]] + [[ "$output" == *"/backend"* ]] + [[ "$output" == *"config --local core.bare false"* ]] + [ ! -e ".worktrees/corrupt-source/backend" ] +} + @test "worktree create --all creates worktrees for all repos" { run "$META_BIN" git worktree create full-task --all [ "$status" -eq 0 ] diff --git a/tests/worktree_cloud.bats b/tests/worktree_cloud.bats index 0d4740a..e713234 100644 --- a/tests/worktree_cloud.bats +++ b/tests/worktree_cloud.bats @@ -1,10 +1,13 @@ #!/usr/bin/env bats +load "${BATS_TEST_DIRNAME}/helpers/git_environment.bash" + # Integration tests for `meta git worktree` cloud/agent extensions (meta-6) # Tests: --meta, --ephemeral, --ttl, --from-ref, prune, lifecycle hooks, # context detection, ephemeral exec, centralized store setup() { + clear_git_local_env META_BIN="$BATS_TEST_DIRNAME/../target/debug/meta" META_GIT_BIN="$BATS_TEST_DIRNAME/../target/debug/meta-git" diff --git a/tests/worktree_edge_cases.bats b/tests/worktree_edge_cases.bats index 69ee518..cb65d0a 100644 --- a/tests/worktree_edge_cases.bats +++ b/tests/worktree_edge_cases.bats @@ -1,9 +1,12 @@ #!/usr/bin/env bats +load "${BATS_TEST_DIRNAME}/helpers/git_environment.bash" + # Integration tests for meta git worktree edge cases (Phase 2 & 3) # Tests: strict mode, prune with orphan detection, cache invalidation, ahead/behind setup() { + clear_git_local_env META_BIN="$BATS_TEST_DIRNAME/../target/debug/meta" META_GIT_BIN="$BATS_TEST_DIRNAME/../target/debug/meta-git"