Compare commits

...
Sign in to create a new pull request.

10 commits

Author SHA1 Message Date
586e891ad8
chore: update deps
Some checks failed
checks / pre-commit (push) Successful in 3m26s
checks / tests-10 (push) Failing after 1m40s
checks / tests-11 (push) Failing after 2m20s
checks / tests-12 (push) Failing after 1m24s
2024-09-23 13:04:28 +02:00
7a1cdaa003
feat: add git status subcommand
Some checks failed
checks / tests-10 (push) Failing after 4m32s
checks / pre-commit (push) Successful in 4m56s
checks / tests-12 (push) Failing after 2m45s
checks / tests-11 (push) Failing after 3m5s
2024-09-10 22:54:24 +02:00
d7bf338d96
chore: update deps
Some checks failed
checks / tests-10 (push) Failing after 2m45s
checks / pre-commit (push) Successful in 3m51s
checks / tests-11 (push) Failing after 2m22s
checks / tests-12 (push) Failing after 2m7s
2024-09-07 16:26:15 +02:00
9dd2405c47
feat: add git pull subcommand
Some checks failed
checks / tests-10 (push) Failing after 2m56s
checks / pre-commit (push) Successful in 4m28s
checks / tests-11 (push) Failing after 4m8s
checks / tests-12 (push) Failing after 2m47s
2024-09-06 20:51:03 +02:00
4746e3b3b1
Add renovate.json
Some checks failed
checks / pre-commit (push) Successful in 2m22s
checks / tests-10 (push) Failing after 1m44s
checks / tests-11 (push) Failing after 1m58s
checks / tests-12 (push) Failing after 2m0s
2024-09-05 11:27:56 +02:00
8076807f22
feat: add git push subcommand 2024-09-04 18:57:27 +02:00
c859c60c8d
feat: add git commit subcommand
All checks were successful
checks / pre-commit (push) Successful in 4m29s
checks / tests-10 (push) Successful in 1m59s
checks / tests-11 (push) Successful in 2m8s
checks / tests-12 (push) Successful in 1m36s
2024-09-03 17:29:44 +02:00
b1ed583a24
chore: set href to halig/actions when clicking the README's build badge
All checks were successful
checks / pre-commit (push) Successful in 2m4s
checks / tests-10 (push) Successful in 2m19s
checks / tests-11 (push) Successful in 1m32s
checks / tests-12 (push) Successful in 1m32s
2024-08-04 15:43:47 +02:00
4a6b3f5c0d
chore: bump version to v0.5.1
All checks were successful
checks / pre-commit (push) Successful in 1m41s
checks / tests-10 (push) Successful in 1m21s
checks / tests-11 (push) Successful in 2m8s
checks / tests-12 (push) Successful in 1m31s
2024-08-04 15:33:06 +02:00
bdb5c984fa
ci: add pre-commit and tests workflows
All checks were successful
checks / pre-commit (push) Successful in 1m39s
checks / tests-10 (push) Successful in 1m17s
checks / tests-11 (push) Successful in 1m10s
checks / tests-12 (push) Successful in 1m13s
2024-08-04 15:23:55 +02:00
29 changed files with 1123 additions and 644 deletions

View file

@ -0,0 +1,73 @@
---
name: checks
on: # yamllint disable-line rule:truthy
- 'push'
jobs:
pre-commit:
runs-on: ubuntu-22.04
steps:
- uses: https://code.forgejo.org/actions/checkout@v4
- uses: https://code.forgejo.org/actions/setup-python@v5
with:
python-version: '3.12'
- name: Setup PDM
uses: pdm-project/setup-pdm@v4
- name: Install dependencies
run: pdm install -G linting
- uses: pre-commit/action@v3.0.1
tests-10:
runs-on: ubuntu-22.04
steps:
- uses: https://code.forgejo.org/actions/checkout@v4
- uses: https://code.forgejo.org/actions/setup-python@v5
with:
python-version: '3.10'
- name: Setup PDM
uses: pdm-project/setup-pdm@v4
- name: Install dependencies
run: pdm install -G testing
- run: make tests
tests-11:
runs-on: ubuntu-22.04
steps:
- uses: https://code.forgejo.org/actions/checkout@v4
- uses: https://code.forgejo.org/actions/setup-python@v5
with:
python-version: '3.11'
- name: Setup PDM
uses: pdm-project/setup-pdm@v4
- name: Install dependencies
run: pdm install -G testing
- run: make tests
tests-12:
runs-on: ubuntu-22.04
steps:
- uses: https://code.forgejo.org/actions/checkout@v4
- uses: https://code.forgejo.org/actions/setup-python@v5
with:
python-version: '3.12'
- name: Setup PDM
uses: pdm-project/setup-pdm@v4
- name: Install dependencies
run: pdm install -G testing
- run: make tests

View file

@ -19,7 +19,7 @@ repos:
args: [ --fix=lf ] args: [ --fix=lf ]
- repo: https://github.com/charliermarsh/ruff-pre-commit - repo: https://github.com/charliermarsh/ruff-pre-commit
rev: v0.4.4 rev: v0.6.4
hooks: hooks:
- id: ruff - id: ruff
args: args:

View file

@ -1,5 +1,5 @@
linters: fmt:
pre-commit run --all-files --color always pre-commit run --all-files --color always
.PHONY: tests .PHONY: tests

View file

@ -1,5 +1,6 @@
# halig # halig
[![Build status](https://git.roboces.dev/catalin/halig/badges/workflows/ci.yaml/badge.svg)](https://git.roboces.dev/catalin/halig/actions)
![PyPI](https://img.shields.io/pypi/v/halig?logo=python) ![PyPI](https://img.shields.io/pypi/v/halig?logo=python)
![PyPI - License](https://img.shields.io/pypi/l/halig) ![PyPI - License](https://img.shields.io/pypi/l/halig)
![PyPI - Python Version](https://img.shields.io/pypi/pyversions/halig) ![PyPI - Python Version](https://img.shields.io/pypi/pyversions/halig)
@ -11,11 +12,10 @@
it encrypts the new contents into an [age](https://github.com/FiloSottile/age) file that it encrypts the new contents into an [age](https://github.com/FiloSottile/age) file that
you can store, _relatively_ safe, anywhere. you can store, _relatively_ safe, anywhere.
## Features ## Features
- Simple notebooks management with paths autocompletion - Simple notebooks management with paths autocompletion
- Passphrase-less, fully-encrypted notes, compatible with existing SSH keys - Passphrase-less, fully-encrypted notes, compatible with existing SSH keys
- No external `age` binary needed - No external `age` binary needed
- Almost all `age` advantages, like having multiple keys for encryption and decryption - Almost all `age` advantages, like having multiple keys for encryption and decryption
- Remote (HTTP) public keys import: e.g: github.com/\<username\>.keys - Remote (HTTP) public keys import: e.g: github.com/\<username\>.keys
@ -35,7 +35,7 @@ mkdir -p "${XDG_CONFIG_HOME:-$HOME/.config}/halig"
cat << EOF > "${XDG_CONFIG_HOME:-$HOME/.config}/halig/halig.yml" cat << EOF > "${XDG_CONFIG_HOME:-$HOME/.config}/halig/halig.yml"
--- ---
notebooks_root_path: ~/Documents/Notebooks notebooks_root_path: ~/Documents/Notebooks
identity_paths: identity_paths:
- ~/.ssh/id_ed25519 - ~/.ssh/id_ed25519
recipient_paths: recipient_paths:
- ~/.ssh/id_ed25519.pub - ~/.ssh/id_ed25519.pub
@ -50,5 +50,6 @@ EOF
halig edit some_notebook # edit today's note relative to <notebooks_root_path>/some_notebook halig edit some_notebook # edit today's note relative to <notebooks_root_path>/some_notebook
halig edit some_notebook/foo # edit <notebooks_root_path>/some_notebook/foo.age halig edit some_notebook/foo # edit <notebooks_root_path>/some_notebook/foo.age
halig notebooks # list current notebooks halig notebooks # list current notebooks
halig git commit
halig git push
``` ```

View file

@ -1 +1 @@
__version__ = "0.5.0" __version__ = "0.6.2"

View file

@ -31,7 +31,7 @@ class EditCommand(BaseCommand):
self.encryptor = Encryptor(self.settings) self.encryptor = Encryptor(self.settings)
if self.note_path.is_dir(): if self.note_path.is_dir():
self.note_path /= f"{utils.now().date()}.age" self.note_path /= f"{utils.now_as_date()}.age"
if not self.note_path.name.endswith(".age"): if not self.note_path.name.endswith(".age"):
err = f"File {self.note_path.name} is not a valid AGE file" err = f"File {self.note_path.name} is not a valid AGE file"

View file

View file

@ -0,0 +1,28 @@
from pathlib import Path
from git import Repo
from rich import print
from halig.commands.base import BaseCommand
from halig.encryption import Encryptor
from halig.settings import Settings
class GitBaseCommand(BaseCommand):
@staticmethod
def __init_repo(repo_path: Path) -> Repo:
"""Check if `repo_path` is a git repo. If not, initialize it"""
if not (repo_path / ".git").is_dir():
print(f"[yellow] {repo_path} is not a git repo, initializing ...")
Repo.init(repo_path)
return Repo(repo_path)
return Repo(repo_path)
def __init__(self, settings: Settings, message: str | None = None):
super().__init__(settings)
self.settings = settings
self.encryptor = Encryptor(self.settings)
self.message = message or self.settings.default_commit_message
self.repo = self.__init_repo(self.settings.notebooks_root_path)

View file

@ -0,0 +1,14 @@
from halig.commands.git.base import GitBaseCommand
class GitCommitCommand(GitBaseCommand):
def __init__(self, message: str | None = None, *args, **kwargs):
super().__init__(*args, **kwargs)
self.message = message or self.settings.default_commit_message
def run(self):
"""Add all .age files to git and commit them using gitpython"""
self.repo.index.add(
[str(path) for path in self.settings.notebooks_root_path.glob("**/*.age")]
)
self.repo.index.commit(self.message)

View file

@ -0,0 +1,19 @@
from halig.commands.git.base import GitBaseCommand
class GitPullCommand(GitBaseCommand):
def __init__(
self, remotes: list[str] | None = None, ref: str | None = None, *args, **kwargs
):
super().__init__(*args, **kwargs)
self.remotes = remotes
self.ref = ref
def run(self):
"""Pull all changes from the remote git repo"""
if not self.remotes:
self.repo.remotes.origin.pull(self.ref or "main")
return
for remote in self.remotes:
self.repo.remotes[remote].pull(self.ref or "main")

View file

@ -0,0 +1,16 @@
from halig.commands.git.base import GitBaseCommand
class GitPushCommand(GitBaseCommand):
def __init__(self, remotes: list[str] | None = None, *args, **kwargs):
super().__init__(*args, **kwargs)
self.remotes = remotes
def run(self):
"""Push all changes to the remote git repo"""
if not self.remotes:
self.repo.remotes.origin.push()
return
for remote in self.remotes:
self.repo.remotes[remote].push()

View file

@ -0,0 +1,13 @@
from rich import print
from halig.commands.git.base import GitBaseCommand
class GitStatusCommand(GitBaseCommand):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) # pragma: no cover
def run(self):
"""Show the status of the git repo, including unstaged *.age files"""
print(self.repo.git.status()) # pragma: no cover

View file

@ -19,7 +19,7 @@ class ReencryptCommand(BaseCommand):
new_data = self.encryptor.encrypt(orig_data) new_data = self.encryptor.encrypt(orig_data)
with note_path.open("wb") as fw: with note_path.open("wb") as fw:
fw.write(new_data) fw.write(new_data)
except pyrage.DecryptError: # type: ignore[reportGeneralTypeIssues] except pyrage.DecryptError: # type: ignore[reportGeneralTypeIssues] # noqa: PERF203
print( print(
f"[yellow] Could not reencrypt {note_path} because no matching keys" f"[yellow] Could not reencrypt {note_path} because no matching keys"
f" were found, skipping ...", f" were found, skipping ...",

View file

@ -19,7 +19,7 @@ class ShowCommand(BaseCommand):
self.plain = plain self.plain = plain
self.console = Console() self.console = Console()
if self.note_path.is_dir(): if self.note_path.is_dir():
self.note_path /= f"{utils.now().date()}.age" self.note_path /= f"{utils.now_as_date()}.age"
if not self.note_path.exists(): if not self.note_path.exists():
err = f"File {self.note_path.name} does not exist" err = f"File {self.note_path.name} does not exist"

View file

@ -1,3 +1,5 @@
# APPS
GIT_HELP = "Git integration"
# COMMANDS # COMMANDS
COMMANDS_NOTEBOOKS_HELP = "List all notebooks and notes, tree-style" COMMANDS_NOTEBOOKS_HELP = "List all notebooks and notes, tree-style"
COMMANDS_EDIT_HELP = "Edit or add a note into a notebook" COMMANDS_EDIT_HELP = "Edit or add a note into a notebook"
@ -10,6 +12,12 @@ which are indexed into a SQLite FTS5 database located at `~/.cache/halig/halig.d
COMMANDS_REENCRYPT_HELP = """Reencrypt all available notes. This operation is useful COMMANDS_REENCRYPT_HELP = """Reencrypt all available notes. This operation is useful
when new public keys have been added to the config file and you want the notes when new public keys have been added to the config file and you want the notes
to be seen by the new pairing private keys""" to be seen by the new pairing private keys"""
COMMANDS_GIT_COMMIT_HELP = "Commit all .age files to git"
COMMANDS_GIT_PUSH_HELP = "Push all .age files to git"
COMMANDS_GIT_PULL_HELP = "Pull all .age files from git"
COMMANDS_GIT_STATUS_HELP = (
"Show the status of the git repo, including unstaged *.age files"
)
# OPTIONS # OPTIONS
OPTION_CONFIG_HELP = "Configuration file. Must be YAML and schema compatible" OPTION_CONFIG_HELP = "Configuration file. Must be YAML and schema compatible"

View file

@ -9,6 +9,10 @@ from typer import Argument, Option, Typer
from halig import literals from halig import literals
from halig.__version__ import __version__ from halig.__version__ import __version__
from halig.commands.edit import EditCommand from halig.commands.edit import EditCommand
from halig.commands.git.commit import GitCommitCommand
from halig.commands.git.pull import GitPullCommand
from halig.commands.git.push import GitPushCommand
from halig.commands.git.status import GitStatusCommand
from halig.commands.import_unencrypted import ImportCommand from halig.commands.import_unencrypted import ImportCommand
from halig.commands.notebooks import NotebooksCommand from halig.commands.notebooks import NotebooksCommand
from halig.commands.reencrypt import ReencryptCommand from halig.commands.reencrypt import ReencryptCommand
@ -17,7 +21,16 @@ from halig.commands.show import ShowCommand
from halig.settings import load_from_file from halig.settings import load_from_file
from halig.utils import capture from halig.utils import capture
app = Typer(pretty_exceptions_enable=False, pretty_exceptions_show_locals=False) git_app = Typer(
name="git",
help=literals.GIT_HELP,
pretty_exceptions_enable=False,
pretty_exceptions_show_locals=False,
)
app = Typer(
name="halig", pretty_exceptions_enable=False, pretty_exceptions_show_locals=False
)
app.add_typer(git_app)
config_option = Option(None, "--config", "-c", help=literals.OPTION_CONFIG_HELP) config_option = Option(None, "--config", "-c", help=literals.OPTION_CONFIG_HELP)
@ -125,8 +138,9 @@ def import_unencrypted(
def search( def search(
term: str, term: str,
index: bool = Option(False, help=literals.OPTION_INDEX_HELP), index: bool = Option(False, help=literals.OPTION_INDEX_HELP),
config: Optional[Path] = config_option, # noqa: UP007
): ):
settings = load_from_file() settings = load_from_file(config)
command = SearchCommand( command = SearchCommand(
term=term, term=term,
index=index, index=index,
@ -136,14 +150,53 @@ def search(
@app.command(help=literals.COMMANDS_REENCRYPT_HELP) @app.command(help=literals.COMMANDS_REENCRYPT_HELP)
def reencrypt(): def reencrypt(config: Path | None = config_option):
settings = load_from_file() settings = load_from_file(config)
command = ReencryptCommand( command = ReencryptCommand(
settings=settings, settings=settings,
) )
command.run() command.run()
@git_app.command(name="commit", help=literals.COMMANDS_GIT_COMMIT_HELP)
def git_commit(
message: str | None = None,
config: Path | None = config_option,
):
settings = load_from_file(config)
command = GitCommitCommand(settings=settings, message=message)
command.run()
@git_app.command(name="push", help=literals.COMMANDS_GIT_PUSH_HELP)
def git_push(
remotes: list[str] | None = None,
config: Path | None = config_option,
):
settings = load_from_file(config)
command = GitPushCommand(settings=settings, remotes=remotes)
command.run()
@git_app.command(name="pull", help=literals.COMMANDS_GIT_PULL_HELP)
def git_pull(
remotes: list[str] | None = None,
config: Path | None = config_option,
):
settings = load_from_file(config)
command = GitPullCommand(settings=settings, remotes=remotes)
command.run()
@git_app.command(name="status", help=literals.COMMANDS_GIT_STATUS_HELP)
def git_status(
config: Path | None = config_option,
):
settings = load_from_file(config)
command = GitStatusCommand(settings=settings)
command.run()
@app.command(help=literals.COMMANDS_VERSION) @app.command(help=literals.COMMANDS_VERSION)
@capture @capture
def version(): def version():

View file

@ -28,7 +28,7 @@ class Settings(BaseSettings):
cache_path (DirectoryPath): a *valid* path used to cache some stuff, cache_path (DirectoryPath): a *valid* path used to cache some stuff,
particularly remote public keys. Defaults to $XDG_CACHE_HOME/halig particularly remote public keys. Defaults to $XDG_CACHE_HOME/halig
remote_public_keys_timeout (float): time after which the retrieval of external public keys remote_public_keys_timeout (float): time after which the retrieval of external public keys
(e.g. github ssh keys) should be interrupted. Defaults to 0.5. (e.g. GitHub ssh keys) should be interrupted. Defaults to 0.5.
""" """
notebooks_root_path: DirectoryPath notebooks_root_path: DirectoryPath
@ -47,6 +47,7 @@ class Settings(BaseSettings):
), ),
) )
remote_public_keys_timeout: float = 0.5 remote_public_keys_timeout: float = 0.5
default_commit_message: str = "Update notebooks"
@field_validator("identity_paths", "recipient_paths", mode="before") @field_validator("identity_paths", "recipient_paths", mode="before")
@classmethod @classmethod

View file

@ -2,14 +2,16 @@ import sys
from collections.abc import Callable from collections.abc import Callable
from functools import wraps from functools import wraps
import pendulum
from pendulum.tz import local_timezone
from rich import print from rich import print
from whenever import Instant
def now(): def now():
tz = local_timezone() return Instant.now()
return pendulum.now(tz) # type: ignore[reportArgumentType]
def now_as_date():
return now().py_datetime().date()
def capture(fn: Callable): def capture(fn: Callable):

View file

@ -1,26 +0,0 @@
import nox
VERSIONS = ["3.10", "3.11", "3.12"]
@nox.session(python=VERSIONS)
def tests(session):
session.run(
"pdm",
"export",
"-G",
"testing",
"-f",
"requirements",
"-o",
"requirements.txt",
external=True,
)
session.install("-r", "requirements.txt")
session.run("make", "tests", external=True)
session.run("rm", "requirements.txt", external=True)
@nox.session(python=VERSIONS)
def linters(session):
session.run("make", "linters", external=True)

1242
pdm.lock generated

File diff suppressed because it is too large Load diff

View file

@ -4,16 +4,17 @@ authors = [
] ]
requires-python = ">=3.10" requires-python = ">=3.10"
dependencies = [ dependencies = [
"typer>=0.12", "typer>=0",
"rich>=13.3.3", "rich>=13.8",
"pydantic>=2.7", "pydantic>=2.8",
"pyyaml>=6.0", "pyyaml>=6.0",
"pyrage>=1.1", "pyrage>=1",
"pendulum>=3.0", "httpx>=0",
"httpx>=0.27",
"platformdirs>=4.2", "platformdirs>=4.2",
"pydantic-settings>=2.0", "pydantic-settings>=2",
"hishel>=0.0.26", "hishel>=0",
"whenever>=0.6",
"gitpython>=3.1",
] ]
name = "halig" name = "halig"
dynamic = ["version"] dynamic = ["version"]
@ -57,22 +58,22 @@ halig = "halig.main:app"
[project.optional-dependencies] [project.optional-dependencies]
testing = [ testing = [
"pytest>=7.2.2", "pytest>=7",
"pytest-cov>=4.0.0", "pytest-cov>=4",
"pyfakefs>=5.1.0", "pyfakefs>=5",
"pytest-clarity>=1.0.1", "pytest-clarity>=1",
"pytest-reportlog>=0.2.1", "pytest-reportlog>=0",
"pytest-duration-insights>=0.1.1", "pytest-duration-insights>=0",
"pytest-pretty>=1.1.1", "pytest-pretty>=1",
"pytest-mock>=3.10.0", "pytest-mock>=3",
"mock>=5.0.1", "mock>=5",
"faker>=28.1.0",
] ]
linting = [ linting = [
"black>=23.3.0", "ruff>=0.4",
"ruff>=0.1.0", "pyright>=1.1",
"pyright>=1.1.301", "mypy>=1.1",
"mypy>=1.1.1", "types-PyYAML>=6.0",
"types-PyYAML>=6.0.12.9",
] ]
docs = [ docs = [
"mkdocs-material>=9.1.5", "mkdocs-material>=9.1.5",
@ -90,7 +91,7 @@ reportMissingTypeStubs = false
reportAttributeAccessIssue = false reportAttributeAccessIssue = false
[tool.ruff.lint] [tool.ruff.lint]
extend-select = ["W", "C90", "I", "N", "UP", "S", "BLE", "B", "A", "COM", "C4", "DTZ", "T10", "EM", "ISC", "T20", "PT", "RSE", "RET", "SIM", "PTH", "ERA", "PGH", "PL", "TRY", "RUF"] extend-select = ["W", "C90", "I", "N", "UP", "S", "BLE", "B", "A", "COM", "C4", "DTZ", "T10", "EM", "ISC", "T20", "PT", "RSE", "RET", "SIM", "PTH", "ERA", "PGH", "PL", "TRY", "RUF", "FURB", "PERF"]
extend-ignore = ["S101", "ISC002", "COM812", "ISC001"] extend-ignore = ["S101", "ISC002", "COM812", "ISC001"]
[tool.mypy] [tool.mypy]
@ -107,3 +108,10 @@ module = [
"httpx_cache" "httpx_cache"
] ]
ignore_missing_imports = true ignore_missing_imports = true
[tool.coverage.run]
omit = [
"halig/__version__.py",
"halig/literals.py",
"halig/main.py"
]

3
renovate.json Normal file
View file

@ -0,0 +1,3 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json"
}

View file

@ -1,6 +1,5 @@
from pathlib import Path from pathlib import Path
import pendulum
import pytest as pytest import pytest as pytest
from halig import utils from halig import utils
@ -24,10 +23,10 @@ def notes(notebooks_path: Path):
dailies = work / "Dailies" dailies = work / "Dailies"
dailies.mkdir() dailies.mkdir()
dt = pendulum.now() dt = utils.now()
for day_offset in range(10): for day_offset in range(10):
dt = dt.subtract(days=day_offset) dt = dt.subtract(hours=day_offset*24)
(dailies / f"{dt.date()}.age").touch() (dailies / f"{dt.py_datetime().date()}.age").touch()
@pytest.fixture() @pytest.fixture()
@ -56,7 +55,7 @@ def notebooks_command(settings: Settings):
@pytest.fixture() @pytest.fixture()
def current_note(notes, settings, encryptor) -> Path: def current_note(notes, settings, encryptor) -> Path:
note_path = settings.notebooks_root_path / f"{utils.now().date()}.age" note_path = settings.notebooks_root_path / f"{utils.now_as_date()}.age"
note_path.touch() note_path.touch()
data = encryptor.encrypt(b"foo") data = encryptor.encrypt(b"foo")
with note_path.open("wb") as f: with note_path.open("wb") as f:
@ -67,7 +66,7 @@ def current_note(notes, settings, encryptor) -> Path:
@pytest.fixture() @pytest.fixture()
def current_daily(notes, settings, encryptor) -> Path: def current_daily(notes, settings, encryptor) -> Path:
note_path = ( note_path = (
settings.notebooks_root_path / "Work" / "Dailies" / f"{utils.now().date()}.age" settings.notebooks_root_path / "Work" / "Dailies" / f"{utils.now_as_date()}.age"
) )
data = encryptor.encrypt(b"foo") data = encryptor.encrypt(b"foo")
with note_path.open("wb") as f: with note_path.open("wb") as f:

View file

View file

@ -0,0 +1,59 @@
import subprocess
import pytest
from halig.commands.git.commit import GitCommitCommand
from halig.settings import Settings
@pytest.fixture
def command(settings: Settings):
return GitCommitCommand(settings=settings)
def test_repo_is_not_initialized(settings):
"""Given that settings.notebooks_root_path is not a git repo, assert that the command
initializes the repo upon instantiation"""
assert not (settings.notebooks_root_path / ".git").is_dir()
GitCommitCommand(settings=settings)
assert (settings.notebooks_root_path / ".git").is_dir()
def test_repo_is_initialized(settings):
"""Manually initialize a repo in settings.notebooks_root_path and check that the command instantiation
is not reinitializing it"""
p = subprocess.Popen(["git", "init"], cwd=settings.notebooks_root_path)
p.wait()
assert (settings.notebooks_root_path / ".git").is_dir()
GitCommitCommand(settings=settings)
assert (settings.notebooks_root_path / ".git").is_dir()
def test_run(settings, command, faker):
"""Create a bunch of .age and non-.age files and assert that all .age files are added to git and that the commit
message is set"""
for _ in range(10):
random_file = settings.notebooks_root_path / f"{faker.word()}.txt"
random_file.touch()
for _ in range(10):
random_age_file = settings.notebooks_root_path / f"{faker.word()}.age"
random_age_file.touch()
command.run()
assert settings.notebooks_root_path / ".git" / "index"
assert settings.notebooks_root_path / ".git" / "index" / "stage"
assert command.message in command.repo.git.log("--pretty=oneline").splitlines()[0]
assert "nothing added to commit but untracked files present (use \"git add\" to track)" in command.repo.git.status()
assert ".age" not in command.repo.git.status()
def test_custom_commit_message(settings, command, faker):
command.message = faker.word()
command.run()
assert command.message in command.repo.git.log("--pretty=oneline").splitlines()[0]

View file

@ -0,0 +1,37 @@
import shutil
import pytest
from git import Repo
from halig.commands.git.pull import GitPullCommand
@pytest.fixture
def command(settings, faker):
"""Configure a local remote for testing located at settings.notebooks_root_path/../remote, push some .age files to
that remote
"""
command = GitPullCommand(settings=settings)
new_path = shutil.copytree(settings.notebooks_root_path, settings.notebooks_root_path / "../remote")
new_path = new_path.resolve()
command.repo.create_remote("origin", str(new_path))
remote_repo = Repo(new_path)
for _ in range(10):
random_age_file = new_path / f"{faker.word()}.age"
random_age_file.touch()
remote_repo.index.add([str(random_age_file)])
remote_repo.index.commit("Update notebooks")
return command
def test_pull_from_origin(command):
command.run()
def test_pull_from_custom_origin(settings, command):
remote_path = settings.notebooks_root_path / "../remote"
command.repo.create_remote("custom", str(remote_path.resolve()))
command.remotes = ["custom"]
command.run()

View file

@ -0,0 +1,38 @@
import shutil
import pytest
from halig.commands.git.commit import GitCommitCommand
from halig.commands.git.push import GitPushCommand
@pytest.fixture
def command(settings, faker):
"""Configure a local remote for testing"""
commit_command = GitCommitCommand(settings=settings)
new_path = shutil.copytree(settings.notebooks_root_path, settings.notebooks_root_path / "../remote")
new_path = new_path.resolve()
for _ in range(10):
random_age_file = settings.notebooks_root_path / f"{faker.word()}.age"
random_age_file.touch()
commit_command.run()
push_command = GitPushCommand(settings=settings)
push_command.repo.create_remote("origin", str(new_path))
return push_command
def test_push_to_origin(command):
"""Test that the command pushes to the origin remote"""
command.run()
def test_push_to_custom_remote(settings, command):
"""Test that the command pushes to a custom remote"""
remote_path = settings.notebooks_root_path / "../remote"
command.repo.create_remote("custom", str(remote_path.resolve()))
command.remotes = ["custom"]
command.run()

View file

@ -71,19 +71,14 @@ def notebooks_path(tmp_path) -> Path:
@pytest.fixture() @pytest.fixture()
def settings(notebooks_path: Path, halig_ssh_path) -> Settings: def settings(notebooks_path: Path, halig_ssh_path) -> Settings:
return Settings( return Settings(notebooks_root_path=notebooks_path,identity_paths=[halig_ssh_path / "id_ed25519"],recipient_paths=[halig_ssh_path / "id_ed25519.pub"])
notebooks_root_path=notebooks_path,
identity_paths=[halig_ssh_path / "id_ed25519"],
recipient_paths=[halig_ssh_path / "id_ed25519.pub"]
)
@pytest.fixture() @pytest.fixture()
def settings_file_path(halig_config_path: Path, notebooks_path: Path) -> Path: def settings_file_path(settings, halig_config_path: Path, notebooks_path: Path) -> Path:
yaml_file = halig_config_path / "halig.yml" yaml_file = halig_config_path / "halig.yml"
yaml_file.touch() yaml_file.touch()
s = Settings(notebooks_root_path=notebooks_path) s = Settings(notebooks_root_path=notebooks_path, identity_paths=settings.identity_paths, recipient_paths=settings.recipient_paths)
# `.dict()` doesn't serialize some fields that yaml doesn't understand
serialized = json.loads(s.model_dump_json()) serialized = json.loads(s.model_dump_json())
with yaml_file.open("w") as f: with yaml_file.open("w") as f:
yaml.safe_dump(serialized, f) yaml.safe_dump(serialized, f)

View file

@ -6,7 +6,8 @@ from halig.settings import Settings, load_from_file
def test_settings_from_env(settings: Settings, notebooks_root_path_envvar): def test_settings_from_env(settings: Settings, notebooks_root_path_envvar):
from_env_settings = Settings() # type: ignore[call-arg] from_env_settings = Settings(recipient_paths=settings.recipient_paths,
identity_paths=settings.identity_paths) # type: ignore[call-arg]
assert from_env_settings.notebooks_root_path == settings.notebooks_root_path assert from_env_settings.notebooks_root_path == settings.notebooks_root_path
@ -29,3 +30,10 @@ def test_load_from_non_existing_file_path_raises_file_not_found_error(halig_conf
file = halig_config_path / "some_invalid_file.yml" file = halig_config_path / "some_invalid_file.yml"
with pytest.raises(FileNotFoundError, match=f"File {file} does not exist"): with pytest.raises(FileNotFoundError, match=f"File {file} does not exist"):
load_from_file(file) load_from_file(file)
def test_settings_identity_paths_is_not_list_is_converted(settings):
s = Settings(identity_paths=settings.identity_paths[0], recipient_paths=settings.recipient_paths[0],
notebooks_root_path=settings.notebooks_root_path)
assert s.identity_paths == [settings.identity_paths[0]]
assert s.recipient_paths == [settings.recipient_paths[0]]