feat: add git commit subcommand
This commit is contained in:
parent
b1ed583a24
commit
c859c60c8d
11 changed files with 628 additions and 429 deletions
|
|
@ -19,7 +19,7 @@ repos:
|
|||
args: [ --fix=lf ]
|
||||
|
||||
- repo: https://github.com/charliermarsh/ruff-pre-commit
|
||||
rev: v0.5.6
|
||||
rev: v0.6.3
|
||||
hooks:
|
||||
- id: ruff
|
||||
args:
|
||||
|
|
|
|||
10
README.md
10
README.md
|
|
@ -1,4 +1,5 @@
|
|||
# halig
|
||||
|
||||
[](https://git.roboces.dev/catalin/halig/actions)
|
||||

|
||||

|
||||
|
|
@ -11,11 +12,10 @@
|
|||
it encrypts the new contents into an [age](https://github.com/FiloSottile/age) file that
|
||||
you can store, _relatively_ safe, anywhere.
|
||||
|
||||
|
||||
## Features
|
||||
|
||||
- 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
|
||||
- Almost all `age` advantages, like having multiple keys for encryption and decryption
|
||||
- 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"
|
||||
---
|
||||
notebooks_root_path: ~/Documents/Notebooks
|
||||
identity_paths:
|
||||
identity_paths:
|
||||
- ~/.ssh/id_ed25519
|
||||
recipient_paths:
|
||||
- ~/.ssh/id_ed25519.pub
|
||||
|
|
@ -50,5 +50,7 @@ EOF
|
|||
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 notebooks # list current notebooks
|
||||
halig git commit
|
||||
halig git pull
|
||||
halig git push
|
||||
```
|
||||
|
||||
|
|
|
|||
0
halig/commands/git/__init__.py
Normal file
0
halig/commands/git/__init__.py
Normal file
35
halig/commands/git/commit.py
Normal file
35
halig/commands/git/commit.py
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
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 GitCommitCommand(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)
|
||||
|
||||
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)
|
||||
|
|
@ -1,3 +1,5 @@
|
|||
# APPS
|
||||
GIT_HELP = "Git integration"
|
||||
# COMMANDS
|
||||
COMMANDS_NOTEBOOKS_HELP = "List all notebooks and notes, tree-style"
|
||||
COMMANDS_EDIT_HELP = "Edit or add a note into a notebook"
|
||||
|
|
@ -10,6 +12,7 @@ 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
|
||||
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"""
|
||||
COMMANDS_GIT_COMMIT_HELP = """Commit all .age files to git"""
|
||||
|
||||
# OPTIONS
|
||||
OPTION_CONFIG_HELP = "Configuration file. Must be YAML and schema compatible"
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ from typer import Argument, Option, Typer
|
|||
from halig import literals
|
||||
from halig.__version__ import __version__
|
||||
from halig.commands.edit import EditCommand
|
||||
from halig.commands.git.commit import GitCommitCommand
|
||||
from halig.commands.import_unencrypted import ImportCommand
|
||||
from halig.commands.notebooks import NotebooksCommand
|
||||
from halig.commands.reencrypt import ReencryptCommand
|
||||
|
|
@ -17,7 +18,16 @@ from halig.commands.show import ShowCommand
|
|||
from halig.settings import load_from_file
|
||||
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)
|
||||
|
||||
|
|
@ -125,8 +135,9 @@ def import_unencrypted(
|
|||
def search(
|
||||
term: str,
|
||||
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(
|
||||
term=term,
|
||||
index=index,
|
||||
|
|
@ -136,14 +147,24 @@ def search(
|
|||
|
||||
|
||||
@app.command(help=literals.COMMANDS_REENCRYPT_HELP)
|
||||
def reencrypt():
|
||||
settings = load_from_file()
|
||||
def reencrypt(config: Path | None = config_option):
|
||||
settings = load_from_file(config)
|
||||
command = ReencryptCommand(
|
||||
settings=settings,
|
||||
)
|
||||
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()
|
||||
|
||||
|
||||
@app.command(help=literals.COMMANDS_VERSION)
|
||||
@capture
|
||||
def version():
|
||||
|
|
|
|||
|
|
@ -47,6 +47,7 @@ class Settings(BaseSettings):
|
|||
),
|
||||
)
|
||||
remote_public_keys_timeout: float = 0.5
|
||||
default_commit_message: str = "Update notebooks"
|
||||
|
||||
@field_validator("identity_paths", "recipient_paths", mode="before")
|
||||
@classmethod
|
||||
|
|
|
|||
|
|
@ -4,16 +4,17 @@ authors = [
|
|||
]
|
||||
requires-python = ">=3.10"
|
||||
dependencies = [
|
||||
"typer>=0.12",
|
||||
"rich>=13.3",
|
||||
"pydantic>=2.7",
|
||||
"typer>=0",
|
||||
"rich>=13.8",
|
||||
"pydantic>=2.8",
|
||||
"pyyaml>=6.0",
|
||||
"pyrage>=1.1",
|
||||
"httpx>=0.27",
|
||||
"pyrage>=1",
|
||||
"httpx>=0",
|
||||
"platformdirs>=4.2",
|
||||
"pydantic-settings>=2.0",
|
||||
"hishel>=0.0",
|
||||
"pydantic-settings>=2",
|
||||
"hishel>=0",
|
||||
"whenever>=0.6.6",
|
||||
"gitpython>=3.1.43",
|
||||
]
|
||||
name = "halig"
|
||||
dynamic = ["version"]
|
||||
|
|
@ -57,22 +58,22 @@ halig = "halig.main:app"
|
|||
|
||||
[project.optional-dependencies]
|
||||
testing = [
|
||||
"pytest>=7.2.2",
|
||||
"pytest-cov>=4.0.0",
|
||||
"pyfakefs>=5.1.0",
|
||||
"pytest-clarity>=1.0.1",
|
||||
"pytest-reportlog>=0.2.1",
|
||||
"pytest-duration-insights>=0.1.1",
|
||||
"pytest-pretty>=1.1.1",
|
||||
"pytest-mock>=3.10.0",
|
||||
"mock>=5.0.1",
|
||||
"pytest>=7",
|
||||
"pytest-cov>=4",
|
||||
"pyfakefs>=5",
|
||||
"pytest-clarity>=1",
|
||||
"pytest-reportlog>=0",
|
||||
"pytest-duration-insights>=0",
|
||||
"pytest-pretty>=1",
|
||||
"pytest-mock>=3",
|
||||
"mock>=5",
|
||||
"faker>=28.1.0",
|
||||
]
|
||||
linting = [
|
||||
"black>=23.3.0",
|
||||
"ruff>=0.1.0",
|
||||
"pyright>=1.1.301",
|
||||
"mypy>=1.1.1",
|
||||
"types-PyYAML>=6.0.12.9",
|
||||
"ruff>=0.4",
|
||||
"pyright>=1.1",
|
||||
"mypy>=1.1",
|
||||
"types-PyYAML>=6.0",
|
||||
]
|
||||
docs = [
|
||||
"mkdocs-material>=9.1.5",
|
||||
|
|
|
|||
0
tests/commands/test_git/__init__.py
Normal file
0
tests/commands/test_git/__init__.py
Normal file
59
tests/commands/test_git/test_commit.py
Normal file
59
tests/commands/test_git/test_commit.py
Normal 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)
|
||||
|
||||
|
||||
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)
|
||||
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)
|
||||
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]
|
||||
Loading…
Add table
Add a link
Reference in a new issue