Skip to content

Commit 4a49cfd

Browse files
hsbtclaude
andcommitted
Prevent test git commands from mutating the checkout's own git config
When a fixture directory has no .git, git repository discovery walks up into the checkout itself, and a fixture `git config user.email` lands in the checkout's shared .git/config, polluting the author identity of subsequent commits in every worktree. Set GIT_CEILING_DIRECTORIES so discovery can never escape tmp/, make the spec git helper refuse local config writes outside tmp/, and fix a show_spec setup line that silently skipped `git init` because sys_exec does not go through a shell. The rubygems test helper gets the same ceiling since its tmp also lives inside the checkout by default. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent d37e5fa commit 4a49cfd

4 files changed

Lines changed: 45 additions & 4 deletions

File tree

spec/commands/show_spec.rb

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,13 @@
164164
before :each do
165165
build_git "foo", path: lib_path("foo")
166166
File.open(lib_path("foo/Gemfile"), "w") {|f| f.puts "gemspec" }
167-
sys_exec "rm -rf .git && git init", dir: lib_path("foo")
167+
# sys_exec does not go through a shell, so this cannot be a single
168+
# `rm -rf .git && git init` command: `&&` would be passed to `rm` as a
169+
# literal argument, silently skipping the `git init` part and leaving a
170+
# non-repository directory from which git would discover the rubygems
171+
# checkout itself.
172+
FileUtils.rm_rf lib_path("foo/.git")
173+
git "init", lib_path("foo")
168174
end
169175

170176
it "does not output git errors" do

spec/spec_helper.rb

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,14 @@ def self.ruby=(ruby)
147147
ENV["GIT_CONFIG_NOSYSTEM"] = "1"
148148
end
149149

150+
# Prevent git commands spawned by specs (directly or through Bundler)
151+
# from discovering the rubygems checkout itself when run in a directory
152+
# that is not a fixture repository, e.g. a fixture whose .git has been
153+
# deleted by a concurrent cleanup. Without this, repository discovery
154+
# walks up into the checkout and a stray `git config` writes the fixture
155+
# identity to the checkout's own (possibly worktree-shared) .git/config.
156+
ENV["GIT_CEILING_DIRECTORIES"] = [Spec::Path.tmp_root.to_s, Spec::Path.source_root.to_s].uniq.join(File::PATH_SEPARATOR)
157+
150158
# Disable git background maintenance. Since Git 2.46, commands like
151159
# `git commit` spawn a detached `git maintenance run --auto` process,
152160
# which briefly creates `.git/objects/maintenance.lock`. That races with

spec/support/subprocess.rb

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# frozen_string_literal: true
22

33
require_relative "command_execution"
4+
require_relative "path"
45

56
module Spec
67
module Subprocess
@@ -31,9 +32,29 @@ def exitstatus
3132
end
3233

3334
def git(cmd, path = Dir.pwd, options = {})
35+
reject_git_config_pollution!(cmd, path)
3436
sh("git #{cmd}", options.merge(dir: path))
3537
end
3638

39+
# A local `git config` write in a directory without a `.git` makes git
40+
# discover an enclosing repository, which can be the rubygems checkout
41+
# itself, polluting its (possibly worktree-shared) `.git/config` with
42+
# fixture identities. Only allow local config writes inside tmp/.
43+
def reject_git_config_pollution!(cmd, path)
44+
require "shellwords"
45+
args = cmd.to_s.shellsplit
46+
return unless args.first == "config"
47+
return if args.any? {|a| ["--global", "--system", "-f", "--file"].include?(a) || a.start_with?("--file=") }
48+
return if args.any? {|a| ["--get", "--get-all", "--get-regexp", "--get-urlmatch", "--list", "-l"].include?(a) }
49+
50+
dir = File.expand_path(path.to_s)
51+
tmp_root = Spec::Path.tmp_root.to_s
52+
return if dir == tmp_root || dir.start_with?(tmp_root + File::SEPARATOR)
53+
54+
raise "Refusing to run `git #{cmd}` in #{dir}: " \
55+
"a local git config write outside tmp/ could end up in the checkout's own .git/config"
56+
end
57+
3758
def sh(cmd, options = {})
3859
dir = options[:dir]
3960
env = options[:env] || {}

test/rubygems/helper.rb

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -373,6 +373,12 @@ def setup
373373

374374
@tempdir = Dir.mktmpdir("test_rubygems_", @tmp)
375375

376+
# @tmp lives inside the checkout by default, so stop git repository
377+
# discovery from walking up into the checkout itself. Otherwise a git
378+
# command run in a non-repository directory under @tempdir could mutate
379+
# the checkout's own (possibly worktree-shared) .git/config.
380+
ENV["GIT_CEILING_DIRECTORIES"] = File.realpath(top_srcdir)
381+
376382
ENV["GEM_VENDOR"] = nil
377383
ENV["GEMRC"] = nil
378384
ENV["XDG_CACHE_HOME"] = nil
@@ -661,9 +667,9 @@ def git_gem(name = "a", version = 1)
661667

662668
Dir.chdir directory do
663669
unless File.exist? ".git"
664-
system @git, "init", "--quiet"
665-
system @git, "config", "user.name", "RubyGems Tests"
666-
system @git, "config", "user.email", "rubygems@example"
670+
system @git, "init", "--quiet", exception: true
671+
system @git, "config", "user.name", "RubyGems Tests", exception: true
672+
system @git, "config", "user.email", "rubygems@example", exception: true
667673
end
668674

669675
system @git, "add", gemspec

0 commit comments

Comments
 (0)