From 61ddc300c1ee8d1f7f48905e4d299fa8d88fcfaf Mon Sep 17 00:00:00 2001 From: Taeer Bar-Yam Date: Thu, 14 Aug 2025 10:32:35 -0400 Subject: [PATCH 1/5] discard string context of git config's core.excludesFile Before this change, any store path in your git config would become part of the string context of any of the parsed strings, and then prevent us from appending it to a path. Upon further reflection, we don't care even if core.excludesFile is a store path itself, so we can unconditionally throw away the string context. --- find-files.nix | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/find-files.nix b/find-files.nix index e52fc06..dc8d5e5 100644 --- a/find-files.nix +++ b/find-files.nix @@ -242,7 +242,10 @@ rec { for (guard (toLower section == "core" && toLower key == "excludesfile")) (_: - resolveFile (home /.) value + # SAFETY: Ultimately this path will get passed into + # `builtins.readFile`, which ignores the context anyways. The + # context is only relevant if it becomes part of a derivation. + resolveFile (home /.) (builtins.unsafeDiscardStringContext value) ) ) ); From 81c6af2553a6c6b3551941018cc4b35a85de4bbf Mon Sep 17 00:00:00 2001 From: Taeer Bar-Yam Date: Mon, 18 Aug 2025 11:40:13 -0400 Subject: [PATCH 2/5] fix comment It turns out readFile does realise the context, so it could make a difference. I would still argue that's not our responsibility and your git config should be pointing ot paths that are already valid. --- find-files.nix | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/find-files.nix b/find-files.nix index dc8d5e5..91317a4 100644 --- a/find-files.nix +++ b/find-files.nix @@ -242,9 +242,11 @@ rec { for (guard (toLower section == "core" && toLower key == "excludesfile")) (_: - # SAFETY: Ultimately this path will get passed into - # `builtins.readFile`, which ignores the context anyways. The - # context is only relevant if it becomes part of a derivation. + # Paths with context can't be appended to other paths, so we have to + # remove the context here. + # SAFETY: gitignore.nix is not responsible for making sure the + # store paths pointed to in your global git config have been + # realised. resolveFile (home /.) (builtins.unsafeDiscardStringContext value) ) ) From 44b4626e81d01f2caa3a6a5a4488017622fdb42e Mon Sep 17 00:00:00 2001 From: Taeer Bar-Yam Date: Wed, 5 Nov 2025 02:43:23 +0100 Subject: [PATCH 3/5] WIP: add regression test for #71 --- tests/default.nix | 61 +++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 57 insertions(+), 4 deletions(-) diff --git a/tests/default.nix b/tests/default.nix index 18844be..bac6b61 100644 --- a/tests/default.nix +++ b/tests/default.nix @@ -3,6 +3,8 @@ let testdata = import ./testdata.nix { inherit pkgs; }; runner = import ./runner.nix { inherit pkgs; }; + gitignoreNix = (import ../. { inherit lib; }); + inherit (gitignoreNix) gitignoreSource; in { plain = runner.makeTest { name = "plain"; rootDir = testdata.sourceUnfiltered + "/test-tree"; }; @@ -10,10 +12,10 @@ in plain-with-testdata-dir = runner.makeTest { name = "plain"; rootDir = testdata.sourceUnfiltered; }; nested-with-testdata-dir = runner.makeTest { name = "nested"; rootDir = testdata.sourceUnfilteredRecursive; }; - + plain-with-testdata-subdir = runner.makeTest { name = "plain"; rootDir = testdata.sourceUnfiltered; subpath = "test-tree"; }; nested-with-testdata-subdir = runner.makeTest { name = "nested"; rootDir = testdata.sourceUnfilteredRecursive; subpath = "test-tree"; }; - + subdir-1 = runner.makeTest { name = "subdir-1"; rootDir = testdata.sourceUnfiltered + "/test-tree"; subpath = "1-simpl"; }; subdir-1x = runner.makeTest { name = "subdir-1x"; rootDir = testdata.sourceUnfiltered + "/test-tree"; subpath = "1-xxxxx"; }; subdir-2 = runner.makeTest { name = "subdir-2"; rootDir = testdata.sourceUnfiltered + "/test-tree"; subpath = "2-negation"; }; @@ -22,6 +24,58 @@ in subdir-9 = runner.makeTest { name = "subdir-9"; rootDir = testdata.sourceUnfiltered + "/test-tree"; subpath = "9-expected"; }; subdir-10 = runner.makeTest { name = "subdir-10"; rootDir = testdata.sourceUnfiltered + "/test-tree"; subpath = "10-subdir-ignoring-itself"; }; + # https://github.com/hercules-ci/gitignore.nix/pull/71 + regression-config-with-store-path = + let + excludesfile = pkgs.writeText "excludesfile" ""; + gitconfig = pkgs.writeText "gitconfig" '' + [core] + excludesfile = ${excludesfile} + ''; + in + pkgs.stdenv.mkDerivation { + name = "config-with-store-path"; + src = testdata.sourceUnfiltered + "/test-tree"; + buildInputs = [ pkgs.nix ]; + NIX_PATH="nixpkgs=${pkgs.path}"; + buildPhase = '' + HOME=/build/HOME + mkdir -p $HOME + + # it must be a symlink to the nix store. + # that way builtins.readFile adds in the relevant context + ln -s ${gitconfig} $HOME/.gitconfig + + export NIX_LOG_DIR=$TMPDIR + export NIX_STATE_DIR=$TMPDIR + + echo --------------- + + # outside of a nix-build, the context of this would be non-empty (replaceing the ''${} with ()) + # inside the nix build, it's empty. + # Arghhhh + + nix-instantiate --eval --expr --strict --json --readonly-mode --option sandbox false \ + 'let pkgs = import {}; in builtins.getContext (builtins.readFile ${pkgs.writeText "foo" (toString pkgs.hello)})' + + echo --------------- + + if nix-instantiate --eval --expr \ + --readonly-mode --option sandbox false \ + '((import ${gitignoreSource ../.} {}).gitignoreSource ./.).outPath' + then touch $out + else + echo + echo "Failed to run with a global excludes file from the nix store." + echo "This may be because the store path gets misinterpreted as a string context." + echo "See https://github.com/hercules-ci/gitignore.nix/pull/71" + exit 1 + fi + ''; + preInstall = ""; + installPhase = ":"; + }; + # Make sure the files aren't added to the store before filtering. shortcircuit = runner.makeTest { name = "nested"; @@ -34,8 +88,7 @@ in }; unit-tests = - let gitignoreNix = import ../default.nix { inherit (pkgs) lib; }; - inherit (gitignoreNix) gitignoreFilterWith gitignoreSourceWith; + let inherit (gitignoreNix) gitignoreFilterWith gitignoreSourceWith; example = gitignoreFilterWith { basePath = ./.; extraRules = '' *.foo !*.bar From ef9193727672eb2776c552195efb5a0a53c35b82 Mon Sep 17 00:00:00 2001 From: Robert Hensing Date: Mon, 10 Nov 2025 21:28:14 +0100 Subject: [PATCH 4/5] Fix guardNonEmptyString One of the simplest mistakes. I use more TDD nowadays... --- find-files.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/find-files.nix b/find-files.nix index 91317a4..085edc7 100644 --- a/find-files.nix +++ b/find-files.nix @@ -295,7 +295,7 @@ rec { */ # TODO: get something like builtins.pathType or builtins.stat into Nix guardFile = p: if pathExists p then [p] else []; - guardNonEmptyString = s: if s == "" then [s] else []; + guardNonEmptyString = s: if s == "" then [] else [s]; guardNonNull = a: if a != null then a else []; From cd6e427877711d2dc840447a461cceb19ebfb7cb Mon Sep 17 00:00:00 2001 From: Robert Hensing Date: Mon, 10 Nov 2025 21:48:30 +0100 Subject: [PATCH 5/5] Make regression-config-with-store-path work The tricky bit was that we had to use a different storeDir because the normal store is not a real store in the sandbox. Also HOME apprently wasn't picked up, and XDG_CONFIG_HOME was broken; see previous commit. --- tests/default.nix | 75 ++++++++++++++++++++++++++++++++--------------- 1 file changed, 52 insertions(+), 23 deletions(-) diff --git a/tests/default.nix b/tests/default.nix index bac6b61..b2258d8 100644 --- a/tests/default.nix +++ b/tests/default.nix @@ -26,42 +26,71 @@ in # https://github.com/hercules-ci/gitignore.nix/pull/71 regression-config-with-store-path = - let - excludesfile = pkgs.writeText "excludesfile" ""; - gitconfig = pkgs.writeText "gitconfig" '' - [core] - excludesfile = ${excludesfile} - ''; - in pkgs.stdenv.mkDerivation { name = "config-with-store-path"; src = testdata.sourceUnfiltered + "/test-tree"; buildInputs = [ pkgs.nix ]; NIX_PATH="nixpkgs=${pkgs.path}"; buildPhase = '' - HOME=/build/HOME - mkdir -p $HOME + # Set up an alternate nix store with a different store directory + export TEST_ROOT=$(pwd)/test-root + export NIX_BUILD_HOOK= + export NIX_CONF_DIR=$TEST_ROOT/etc + export NIX_LOCALSTATE_DIR=$TEST_ROOT/var + export NIX_LOG_DIR=$TEST_ROOT/var/log/nix + export NIX_STATE_DIR=$TEST_ROOT/var/nix + export NIX_STORE_DIR=$TEST_ROOT/store + export NIX_STORE=$TEST_ROOT/store - # it must be a symlink to the nix store. - # that way builtins.readFile adds in the relevant context - ln -s ${gitconfig} $HOME/.gitconfig + mkdir -p $NIX_STORE_DIR $NIX_CONF_DIR - export NIX_LOG_DIR=$TMPDIR - export NIX_STATE_DIR=$TMPDIR + # Write nix.conf - disable sandbox for nested builds + cat > $NIX_CONF_DIR/nix.conf < {}; + excludesfile = derivation { + name = \"excludesfile\"; + system = builtins.currentSystem; + builder = \"/bin/sh\"; + args = [\"-c\" \"echo -n \\\"\\\" > \\\$out\"]; + }; + in + derivation { + name = \"home-config\"; + system = builtins.currentSystem; + builder = \"${pkgs.bash}/bin/bash\"; + PATH = \"${pkgs.coreutils}/bin\"; + inherit excludesfile; + args = [\"-c\" \" + mkdir -p \\\$out/git + printf '[core]\\\\n excludesfile = %s\\\\n' \\\$excludesfile > \\\$out/git/config + \"]; + } + ") - nix-instantiate --eval --expr --strict --json --readonly-mode --option sandbox false \ - 'let pkgs = import {}; in builtins.getContext (builtins.readFile ${pkgs.writeText "foo" (toString pkgs.hello)})' - - echo --------------- + # Use XDG_CONFIG_HOME pointing directly to this store path (like home-manager does) + export XDG_CONFIG_HOME=$configdir + echo "XDG_CONFIG_HOME is now: $XDG_CONFIG_HOME" + echo "Config file contents:" + cat $XDG_CONFIG_HOME/git/config + echo "---------------" + echo "Testing gitignoreSource with store path in git config:" + # This calls the real main code (gitignoreSource) which will naturally + # evaluate globalConfiguredExcludesFile when building the pattern tree + # Without the fix: fails with "a string that refers to a store path cannot be appended to a path" + # With the fix: succeeds (unsafeDiscardStringContext strips the context) if nix-instantiate --eval --expr \ - --readonly-mode --option sandbox false \ '((import ${gitignoreSource ../.} {}).gitignoreSource ./.).outPath' then touch $out else