Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,7 @@ Put cross-project preferences (timezone, cooldowns) in `~/.remember/config.json`
| `cooldowns.git_backup_seconds` | `900` | Minimum seconds between auto-backup commits (no-op if `~/.remember/` is not a git repo) |
| `git_backup.remote` | _(empty)_ | Remote to push memory backups to. Empty → bare `git push`, relying on the branch's upstream tracking (the standard `origin main` setup). Set this if you have multiple remotes or a non-standard tracking config. |
| `git_backup.branch` | _(empty)_ | Branch to push to. Only used when `git_backup.remote` is set; empty pushes the current branch. The resolved remote/branch is logged on the first push. |
| `git_backup.gpg_sign` | `false` | Sign auto-backup commits. Default passes `--no-gpg-sign` so background commits never hang on a passphrase prompt. Set `true` only with non-interactive signing (e.g. a hardware key) to honour your global `commit.gpgSign`. |
| `thresholds.min_human_messages` | `3` | Minimum messages before saving |
| `thresholds.delta_lines_trigger` | `50` | Tool call output lines that trigger auto-save |
| `thresholds.extract_max_bytes` | `300000` | Max UTF-8 size of the session extract sent to Haiku. Larger extracts are truncated to their most-recent tail so a very long session can't overflow the model's context window and silently stall saves. `0` disables the cap. |
Expand Down
11 changes: 10 additions & 1 deletion hooks.d/after_save/50-git-backup.sh
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,15 @@ fi
GIT_BACKUP_BRANCH=$(config ".git_backup.branch" "")
REMOTE_NAME="${GIT_BACKUP_REMOTE:-origin}"

# ── Configurable commit signing (#62) ─────────────────────────────────────
# We pass --no-gpg-sign by default so background commits never hang on a
# passphrase prompt. Users with non-interactive signing (e.g. a hardware key)
# can set git_backup.gpg_sign=true to drop the flag and honour their own
# commit.gpgSign config. Empty flag (unquoted) = no extra arg.
GIT_BACKUP_GPG_SIGN=$(config ".git_backup.gpg_sign" "false")
GPG_SIGN_FLAG="--no-gpg-sign"
[ "$GIT_BACKUP_GPG_SIGN" = "true" ] && GPG_SIGN_FLAG=""

_push() {
if [ -n "$GIT_BACKUP_REMOTE" ]; then
GIT_TERMINAL_PROMPT=0 git -C "$REPO_ROOT" push "$GIT_BACKUP_REMOTE" ${GIT_BACKUP_BRANCH:+"$GIT_BACKUP_BRANCH"} >/dev/null 2>&1
Expand Down Expand Up @@ -125,7 +134,7 @@ fi
fi

TS=$(_remember_date '+%H:%M')
if git -C "$REPO_ROOT" commit --no-gpg-sign \
if git -C "$REPO_ROOT" commit $GPG_SIGN_FLAG \
-m "auto: $SLUG $TS" \
-- "$SLUG/" >/dev/null 2>&1; then
log "git-backup" "committed $SLUG"
Expand Down
77 changes: 77 additions & 0 deletions tests/test_git_backup_hook.py
Original file line number Diff line number Diff line change
Expand Up @@ -608,3 +608,80 @@ def _ref_count(bare, ref="refs/heads/main"):
assert log_files
log_content = log_files[0].read_text()
assert "remote 'backup'" in log_content


def _configure_fake_gpg(remember: Path, tmp_path: Path) -> None:
"""Point the repo at a stub gpg that emits a fake signature and enable commit.gpgsign.

Lets us assert signing behaviour deterministically in CI without a real GPG key:
git embeds whatever the stub prints (and trusts the SIG_CREATED status line).
"""
fake = tmp_path / "fakegpg.sh"
fake.write_text(
"#!/bin/sh\n"
'echo "[GNUPG:] SIG_CREATED G" >&2\n'
"cat <<'SIG'\n"
"-----BEGIN PGP SIGNATURE-----\n"
"\n"
"fakefakefake\n"
"-----END PGP SIGNATURE-----\n"
"SIG\n"
)
fake.chmod(0o755)
_git(remember, ["config", "gpg.program", str(fake)])
_git(remember, ["config", "commit.gpgsign", "true"])
_git(remember, ["config", "user.signingkey", "FAKEKEY"])


def _is_signed(repo: Path, ref: str = "HEAD") -> bool:
"""True if the commit object carries a gpgsig header."""
r = subprocess.run(
["git", "-C", str(repo), "cat-file", "-p", ref],
capture_output=True, text=True, check=True,
)
return "gpgsig" in r.stdout


class TestGitBackupGpgSign:
"""Tests for configurable commit signing via git_backup.gpg_sign (#62)."""

def test_default_commit_is_not_signed(self, tmp_path):
"""Default (no git_backup.gpg_sign): --no-gpg-sign is passed, overriding repo commit.gpgsign."""
home, remember, remote = make_external_remember_repo(tmp_path)
_configure_fake_gpg(remember, tmp_path)
slug = "test-slug"
slug_dir = remember / slug
slug_dir.mkdir()
(slug_dir / "now.md").write_text("## 10:00 | test\nMemory.\n")

project = tmp_path / "project"
project.mkdir()
cfg = _make_config(tmp_path, cooldown=0)

result = _run_hook(slug_dir, project, home, config_path=cfg)
assert result.returncode == 0
wait_for_lock_release(remember / ".git-backup.lock")

assert len(_commit_log(remember)) == 2
assert not _is_signed(remember), "default commit must stay unsigned (--no-gpg-sign)"

def test_gpg_sign_true_honors_user_signing(self, tmp_path):
"""git_backup.gpg_sign=true omits --no-gpg-sign, so the repo's commit.gpgsign is honored."""
home, remember, remote = make_external_remember_repo(tmp_path)
_configure_fake_gpg(remember, tmp_path)
slug = "test-slug"
slug_dir = remember / slug
slug_dir.mkdir()
(slug_dir / "now.md").write_text("## 10:00 | test\nMemory.\n")

project = tmp_path / "project"
project.mkdir()
cfg = tmp_path / "gpg-config.json"
cfg.write_text('{"cooldowns": {"git_backup_seconds": 0}, "git_backup": {"gpg_sign": true}}')

result = _run_hook(slug_dir, project, home, config_path=cfg)
assert result.returncode == 0
wait_for_lock_release(remember / ".git-backup.lock")

assert len(_commit_log(remember)) == 2
assert _is_signed(remember), "gpg_sign=true must let the repo sign the commit"
Loading