From 6a359c0a2f0b786d6ca7ae19b663abf6907d6e08 Mon Sep 17 00:00:00 2001 From: DavHau Date: Thu, 21 Sep 2023 14:07:54 +0200 Subject: [PATCH] clan-cli: add git.commit_file() to auto commit files if inside a git - commit only if inside a git repo - commit only the specified file and nothing else - auto-generate commit message if not specified --- pkgs/clan-cli/clan_cli/config/__init__.py | 5 +++ pkgs/clan-cli/clan_cli/config/machine.py | 2 + pkgs/clan-cli/clan_cli/git.py | 51 +++++++++++++++++++++++ pkgs/clan-cli/default.nix | 4 ++ pkgs/clan-cli/flake-module.nix | 12 +++--- pkgs/clan-cli/tests/conftest.py | 21 ++++++++++ pkgs/clan-cli/tests/test_config.py | 1 + pkgs/clan-cli/tests/test_git.py | 40 ++++++++++++++++++ 8 files changed, 131 insertions(+), 5 deletions(-) create mode 100644 pkgs/clan-cli/clan_cli/git.py create mode 100644 pkgs/clan-cli/tests/test_git.py diff --git a/pkgs/clan-cli/clan_cli/config/__init__.py b/pkgs/clan-cli/clan_cli/config/__init__.py index 1a3c0fbd..c5c00047 100644 --- a/pkgs/clan-cli/clan_cli/config/__init__.py +++ b/pkgs/clan-cli/clan_cli/config/__init__.py @@ -9,6 +9,7 @@ from typing import Any, Optional, Type from clan_cli.dirs import get_clan_flake_toplevel from clan_cli.errors import ClanError +from clan_cli.git import commit_file from clan_cli.machines.folders import machine_settings_file from clan_cli.nix import nix_eval @@ -240,6 +241,10 @@ def set_option( settings_file.parent.mkdir(parents=True, exist_ok=True) with open(settings_file, "w") as f: json.dump(new_config, f, indent=2) + if settings_file.resolve().is_relative_to(get_clan_flake_toplevel()): + commit_file( + settings_file, commit_message=f"Set option {option_description}" + ) # takes a (sub)parser and configures it diff --git a/pkgs/clan-cli/clan_cli/config/machine.py b/pkgs/clan-cli/clan_cli/config/machine.py index cad6b508..4e6be49f 100644 --- a/pkgs/clan-cli/clan_cli/config/machine.py +++ b/pkgs/clan-cli/clan_cli/config/machine.py @@ -7,6 +7,7 @@ from typing import Optional from fastapi import HTTPException from clan_cli.dirs import get_clan_flake_toplevel, nixpkgs_source +from clan_cli.git import commit_file from clan_cli.machines.folders import machine_folder, machine_settings_file from clan_cli.nix import nix_eval @@ -36,6 +37,7 @@ def set_config_for_machine(machine_name: str, config: dict) -> None: settings_path.parent.mkdir(parents=True, exist_ok=True) with open(settings_path, "w") as f: json.dump(config, f) + commit_file(settings_path) def schema_for_machine(machine_name: str, flake: Optional[Path] = None) -> dict: diff --git a/pkgs/clan-cli/clan_cli/git.py b/pkgs/clan-cli/clan_cli/git.py new file mode 100644 index 00000000..bb2d7cec --- /dev/null +++ b/pkgs/clan-cli/clan_cli/git.py @@ -0,0 +1,51 @@ +import subprocess +from pathlib import Path +from typing import Optional + +from clan_cli.dirs import get_clan_flake_toplevel +from clan_cli.errors import ClanError +from clan_cli.nix import nix_shell + + +# generic vcs agnostic commit function +def commit_file( + file_path: Path, + repo_dir: Optional[Path] = None, + commit_message: Optional[str] = None, +) -> None: + # set default for repo_dir + if repo_dir is None: + repo_dir = get_clan_flake_toplevel() + # check that the file is in the git repository and exists + if not Path(file_path).resolve().is_relative_to(repo_dir.resolve()): + raise ClanError(f"File {file_path} is not in the git repository {repo_dir}") + if not file_path.exists(): + raise ClanError(f"File {file_path} does not exist") + # generate commit message if not provided + if commit_message is None: + # ensure that mentioned file path is relative to repo + commit_message = f"Add {file_path.relative_to(repo_dir)}" + # check if the repo is a git repo and commit + if (repo_dir / ".git").exists(): + _commit_file_to_git(repo_dir, file_path, commit_message) + else: + return + + +def _commit_file_to_git(repo_dir: Path, file_path: Path, commit_message: str) -> None: + """Commit a file to a git repository. + + :param repo_dir: The path to the git repository. + :param file_path: The path to the file to commit. + :param commit_message: The commit message. + :raises ClanError: If the file is not in the git repository. + """ + # add the file to the git index + subprocess.run(["git", "add", file_path], cwd=repo_dir, check=True) + # commit only that file + cmd = nix_shell(["git"], ["git", "commit", "-m", commit_message, file_path.name]) + subprocess.run( + cmd, + cwd=repo_dir, + check=True, + ) diff --git a/pkgs/clan-cli/default.nix b/pkgs/clan-cli/default.nix index 7f601c26..4bd65f6a 100644 --- a/pkgs/clan-cli/default.nix +++ b/pkgs/clan-cli/default.nix @@ -20,6 +20,7 @@ , rsync , pkgs , ui-assets +, lib }: let @@ -92,6 +93,9 @@ python3.pkgs.buildPythonPackage { chmod +w -R ./src cd ./src + # git is needed for test_git.py + export PATH="${lib.makeBinPath [pkgs.git]}:$PATH" + export NIX_STATE_DIR=$TMPDIR/nix IN_NIX_SANDBOX=1 ${checkPython}/bin/python -m pytest -m "not impure" -s ./tests touch $out diff --git a/pkgs/clan-cli/flake-module.nix b/pkgs/clan-cli/flake-module.nix index d9da6b30..9d3b88cf 100644 --- a/pkgs/clan-cli/flake-module.nix +++ b/pkgs/clan-cli/flake-module.nix @@ -12,15 +12,17 @@ ## Optional dependencies for clan cli, we re-expose them here to make sure they all build. inherit (pkgs) + age bash bubblewrap + git openssh - sshpass - zbar - tor - age rsync - sops; + sops + sshpass + tor + zbar + ; # Override license so that we can build zerotierone without # having to re-import nixpkgs. zerotierone = pkgs.zerotierone.overrideAttrs (_old: { meta = { }; }); diff --git a/pkgs/clan-cli/tests/conftest.py b/pkgs/clan-cli/tests/conftest.py index c502c73e..925c7cad 100644 --- a/pkgs/clan-cli/tests/conftest.py +++ b/pkgs/clan-cli/tests/conftest.py @@ -1,5 +1,11 @@ import os +import subprocess import sys +from pathlib import Path + +import pytest + +from clan_cli.nix import nix_shell sys.path.append(os.path.join(os.path.dirname(__file__), "helpers")) @@ -14,3 +20,18 @@ pytest_plugins = [ "host_group", "test_flake", ] + + +# fixture for git_repo +@pytest.fixture +def git_repo(tmp_path: Path) -> Path: + # initialize a git repository + cmd = nix_shell(["git"], ["git", "init"]) + subprocess.run(cmd, cwd=tmp_path, check=True) + # set user.name and user.email + cmd = nix_shell(["git"], ["git", "config", "user.name", "test"]) + subprocess.run(cmd, cwd=tmp_path, check=True) + cmd = nix_shell(["git"], ["git", "config", "user.email", "test@test.test"]) + subprocess.run(cmd, cwd=tmp_path, check=True) + # return the path to the git repository + return tmp_path diff --git a/pkgs/clan-cli/tests/test_config.py b/pkgs/clan-cli/tests/test_config.py index c65ef11e..049b12aa 100644 --- a/pkgs/clan-cli/tests/test_config.py +++ b/pkgs/clan-cli/tests/test_config.py @@ -28,6 +28,7 @@ example_options = f"{Path(config.__file__).parent}/jsonschema/options.json" def test_set_some_option( args: list[str], expected: dict[str, Any], + test_flake: Path, ) -> None: # create temporary file for out_file with tempfile.NamedTemporaryFile() as out_file: diff --git a/pkgs/clan-cli/tests/test_git.py b/pkgs/clan-cli/tests/test_git.py new file mode 100644 index 00000000..983dd60d --- /dev/null +++ b/pkgs/clan-cli/tests/test_git.py @@ -0,0 +1,40 @@ +import subprocess +import tempfile +from pathlib import Path + +import pytest + +from clan_cli import git +from clan_cli.errors import ClanError + + +def test_commit_file(git_repo: Path) -> None: + # create a file in the git repo + (git_repo / "test.txt").touch() + # commit the file + git.commit_file((git_repo / "test.txt"), git_repo, "test commit") + # check that the repo directory does in fact contain the file + assert (git_repo / "test.txt").exists() + # check that the working tree is clean + assert not subprocess.check_output(["git", "status", "--porcelain"], cwd=git_repo) + # check that the latest commit message is correct + assert ( + subprocess.check_output( + ["git", "log", "-1", "--pretty=%B"], cwd=git_repo + ).decode("utf-8") + == "test commit\n\n" + ) + + +def test_commit_file_outside_git_raises_error(git_repo: Path) -> None: + # create a file outside the git (a temporary file) + with tempfile.NamedTemporaryFile() as tmp: + # commit the file + with pytest.raises(ClanError): + git.commit_file(Path(tmp.name), git_repo, "test commit") + + +def test_commit_file_not_existing_raises_error(git_repo: Path) -> None: + # commit a file that does not exist + with pytest.raises(ClanError): + git.commit_file(Path("test.txt"), git_repo, "test commit")