From 8e2bc8eed7fadaddc9960f2d117186420fe8067a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Thalheim?= Date: Tue, 21 Jan 2025 09:51:15 +0100 Subject: [PATCH] nixos-rebuild-ng: fix repl behaving differently on git flakes than build commands Since we use builtins.getFlake we have behavior differences between normal nix build and the nix repl because builtins.getFlake won't pick up local flakes as git+file but assumes path:// flakes instead. This can have surprising effects such as beeing able to access untracked files that would lead to build failures otherwise or copying large files to the nix store. --- .../src/nixos_rebuild/models.py | 31 +++++++++++++--- .../nixos-rebuild-ng/src/tests/test_models.py | 36 ++++++++++++++----- .../ni/nixos-rebuild-ng/src/tests/test_nix.py | 13 ++++--- 3 files changed, 64 insertions(+), 16 deletions(-) diff --git a/pkgs/by-name/ni/nixos-rebuild-ng/src/nixos_rebuild/models.py b/pkgs/by-name/ni/nixos-rebuild-ng/src/nixos_rebuild/models.py index c79052f3752fa..90ab6d27f41bd 100644 --- a/pkgs/by-name/ni/nixos-rebuild-ng/src/nixos_rebuild/models.py +++ b/pkgs/by-name/ni/nixos-rebuild-ng/src/nixos_rebuild/models.py @@ -61,6 +61,25 @@ def from_arg(cls, attr: str | None, file: str | None) -> Self: return cls(Path(file or "default.nix"), attr) +def discover_git(location: Path) -> str | None: + current = location.resolve() + previous = None + + while current.is_dir() and current != previous: + dotgit = current / ".git" + if dotgit.is_dir(): + return str(current) + elif dotgit.is_file(): # this is a worktree + with dotgit.open() as f: + dotgit_content = f.read().strip() + if dotgit_content.startswith("gitdir: "): + return dotgit_content.split("gitdir: ")[1] + previous = current + current = current.parent + + return None + + @dataclass(frozen=True) class Flake: path: Path | str @@ -84,11 +103,15 @@ def parse( assert m is not None, f"got no matches for {flake_str}" attr = m.group("attr") nixos_attr = f"nixosConfigurations.{attr or hostname_fn() or 'default'}" - path = m.group("path") - if ":" in path: - return cls(path, nixos_attr) + path_str = m.group("path") + if ":" in path_str: + return cls(path_str, nixos_attr) else: - return cls(Path(path), nixos_attr) + path = Path(path_str) + git_repo = discover_git(path) + if git_repo is not None: + return cls(f"git+file://{git_repo}", nixos_attr) + return cls(path, nixos_attr) @classmethod def from_arg(cls, flake_arg: Any, target_host: Remote | None) -> Self | None: diff --git a/pkgs/by-name/ni/nixos-rebuild-ng/src/tests/test_models.py b/pkgs/by-name/ni/nixos-rebuild-ng/src/tests/test_models.py index 20a5b85cf4558..31d72396c8c43 100644 --- a/pkgs/by-name/ni/nixos-rebuild-ng/src/tests/test_models.py +++ b/pkgs/by-name/ni/nixos-rebuild-ng/src/tests/test_models.py @@ -4,6 +4,8 @@ from typing import Any from unittest.mock import patch +from pytest import MonkeyPatch + import nixos_rebuild.models as m from .helpers import get_qualified_name @@ -30,7 +32,7 @@ def test_build_attr_to_attr() -> None: ) -def test_flake_parse() -> None: +def test_flake_parse(tmpdir: Path, monkeypatch: MonkeyPatch) -> None: assert m.Flake.parse("/path/to/flake#attr") == m.Flake( Path("/path/to/flake"), "nixosConfigurations.attr" ) @@ -40,15 +42,31 @@ def test_flake_parse() -> None: assert m.Flake.parse("/path/to/flake", lambda: "hostname") == m.Flake( Path("/path/to/flake"), "nixosConfigurations.hostname" ) - assert m.Flake.parse(".#attr") == m.Flake(Path("."), "nixosConfigurations.attr") - assert m.Flake.parse("#attr") == m.Flake(Path("."), "nixosConfigurations.attr") - assert m.Flake.parse(".") == m.Flake(Path("."), "nixosConfigurations.default") + # change directory to tmpdir + with monkeypatch.context() as patch_context: + patch_context.chdir(tmpdir) + assert m.Flake.parse(".#attr") == m.Flake(Path("."), "nixosConfigurations.attr") + assert m.Flake.parse("#attr") == m.Flake(Path("."), "nixosConfigurations.attr") + assert m.Flake.parse(".") == m.Flake(Path("."), "nixosConfigurations.default") assert m.Flake.parse("path:/to/flake#attr") == m.Flake( "path:/to/flake", "nixosConfigurations.attr" ) assert m.Flake.parse("github:user/repo/branch") == m.Flake( "github:user/repo/branch", "nixosConfigurations.default" ) + git_root = tmpdir / "git_root" + git_root.mkdir() + (git_root / ".git").mkdir() + assert m.Flake.parse(str(git_root)) == m.Flake( + f"git+file://{git_root}", "nixosConfigurations.default" + ) + + work_tree = tmpdir / "work_tree" + work_tree.mkdir() + (work_tree / ".git").write_text("gitdir: /path/to/git", "utf-8") + assert m.Flake.parse(str(work_tree)) == m.Flake( + "git+file:///path/to/git", "nixosConfigurations.default" + ) def test_flake_to_attr() -> None: @@ -61,7 +79,7 @@ def test_flake_to_attr() -> None: @patch(get_qualified_name(platform.node), autospec=True) -def test_flake_from_arg(mock_node: Any) -> None: +def test_flake_from_arg(mock_node: Any, monkeypatch: MonkeyPatch, tmpdir: Path) -> None: mock_node.return_value = "hostname" # Flake string @@ -73,9 +91,11 @@ def test_flake_from_arg(mock_node: Any) -> None: assert m.Flake.from_arg(False, None) is None # True - assert m.Flake.from_arg(True, None) == m.Flake( - Path("."), "nixosConfigurations.hostname" - ) + with monkeypatch.context() as patch_context: + patch_context.chdir(tmpdir) + assert m.Flake.from_arg(True, None) == m.Flake( + Path("."), "nixosConfigurations.hostname" + ) # None when we do not have /etc/nixos/flake.nix with patch( diff --git a/pkgs/by-name/ni/nixos-rebuild-ng/src/tests/test_nix.py b/pkgs/by-name/ni/nixos-rebuild-ng/src/tests/test_nix.py index 8b548eb14fa50..2c3a6c5ce60c9 100644 --- a/pkgs/by-name/ni/nixos-rebuild-ng/src/tests/test_nix.py +++ b/pkgs/by-name/ni/nixos-rebuild-ng/src/tests/test_nix.py @@ -6,6 +6,7 @@ from unittest.mock import ANY, call, patch import pytest +from pytest import MonkeyPatch import nixos_rebuild.models as m import nixos_rebuild.nix as n @@ -51,7 +52,8 @@ def test_build(mock_run: Any, monkeypatch: Any) -> None: autospec=True, return_value=CompletedProcess([], 0, stdout=" \n/path/to/file\n "), ) -def test_build_flake(mock_run: Any) -> None: +def test_build_flake(mock_run: Any, monkeypatch: MonkeyPatch, tmpdir: Path) -> None: + monkeypatch.chdir(tmpdir) flake = m.Flake.parse(".#hostname") assert n.build_flake( @@ -165,7 +167,10 @@ def run_wrapper_side_effect( autospec=True, return_value=CompletedProcess([], 0, stdout=" \n/path/to/file\n "), ) -def test_build_remote_flake(mock_run: Any, monkeypatch: Any) -> None: +def test_build_remote_flake( + mock_run: Any, monkeypatch: MonkeyPatch, tmpdir: Path +) -> None: + monkeypatch.chdir(tmpdir) flake = m.Flake.parse(".#hostname") build_host = m.Remote("user@host", [], None) monkeypatch.setenv("NIX_SSHOPTS", "--ssh opts") @@ -287,7 +292,7 @@ def test_copy_closure(monkeypatch: Any) -> None: @patch(get_qualified_name(n.run_wrapper, n), autospec=True) def test_edit(mock_run: Any, monkeypatch: Any, tmpdir: Any) -> None: # Flake - flake = m.Flake.parse(".#attr") + flake = m.Flake.parse(f"{tmpdir}#attr") n.edit(flake, {"commit_lock_file": True}) mock_run.assert_called_with( [ @@ -297,7 +302,7 @@ def test_edit(mock_run: Any, monkeypatch: Any, tmpdir: Any) -> None: "edit", "--commit-lock-file", "--", - ".#nixosConfigurations.attr", + f"{tmpdir}#nixosConfigurations.attr", ], check=False, )