Skip to content

Commit 9b00668

Browse files
committed
Step: Create .gemini/hooks/pre-commit.py and tests
1 parent 5ae3353 commit 9b00668

4 files changed

Lines changed: 181 additions & 1 deletion

File tree

.gemini/hooks/pre-commit.py

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
#!/usr/bin/env python3
2+
import os
3+
import subprocess
4+
import sys
5+
from datetime import date
6+
7+
def run_command(command):
8+
result = subprocess.run(command, capture_output=True, text=True, shell=True)
9+
return result
10+
11+
def main():
12+
today = date.today().strftime("%Y-%m-%d")
13+
journal_path = f"journal/{today}.md"
14+
15+
# Scan for changes (staged, modified, untracked)
16+
# git ls-files --modified --others --exclude-standard
17+
res = run_command("git ls-files --modified --others --exclude-standard")
18+
changed_files = res.stdout.strip().splitlines()
19+
20+
if not changed_files:
21+
return 0
22+
23+
# Calculate max(mtime) for all changed files (excluding .gemini/ and the journal)
24+
meaningful_changes = [f for f in changed_files if not f.startswith(".gemini/") and f != journal_path]
25+
26+
if not meaningful_changes:
27+
return 0
28+
29+
max_mtime = 0
30+
for f in meaningful_changes:
31+
if os.path.exists(f):
32+
mtime = os.path.getmtime(f)
33+
if mtime > max_mtime:
34+
max_mtime = mtime
35+
36+
# Check journal mtime
37+
if not os.path.exists(journal_path):
38+
print(f"Error: Updated journal required ({journal_path} does not exist)")
39+
return 1
40+
41+
journal_mtime = os.path.getmtime(journal_path)
42+
43+
if journal_mtime < max_mtime:
44+
print("Error: Updated journal required (Today's journal must be the most recently modified file)")
45+
return 1
46+
47+
# Run make
48+
print("Running validation (make test)...")
49+
res = run_command("make test")
50+
if res.returncode != 0:
51+
print("Validation failed:")
52+
print(res.stdout)
53+
print(res.stderr)
54+
return res.returncode
55+
56+
print("Validation passed.")
57+
return 0
58+
59+
if __name__ == "__main__":
60+
sys.exit(main())

TASKS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ Put done tasks into the Archive.
1717
---
1818

1919
## Active Tasks
20-
- [ ] Consolidate project hooks into a single Git pre-commit hook (See plan: plans/consolidate-hooks.md)
20+
- [/] Consolidate project hooks into a single Git pre-commit hook (@apiad) (See plan: plans/consolidate-hooks.md)
2121

2222
---
2323

makefile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ all: test lint
44

55
test:
66
@echo "Running tests..."
7+
python3 tests/test_hooks.py
78

89
docs-serve:
910
@mkdocs serve

tests/test_hooks.py

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
import os
2+
import unittest
3+
import subprocess
4+
import shutil
5+
import time
6+
from datetime import date
7+
8+
class TestPreCommitHook(unittest.TestCase):
9+
def setUp(self):
10+
self.test_dir = "temp_test_repo"
11+
if os.path.exists(self.test_dir):
12+
shutil.rmtree(self.test_dir)
13+
os.makedirs(self.test_dir)
14+
os.chdir(self.test_dir)
15+
subprocess.run(["git", "init"], capture_output=True)
16+
# Configure user for testing
17+
subprocess.run(["git", "config", "user.email", "test@example.com"], capture_output=True)
18+
subprocess.run(["git", "config", "user.name", "Test User"], capture_output=True)
19+
20+
os.makedirs(".gemini/hooks")
21+
os.makedirs("journal")
22+
23+
# Initial commit
24+
with open(".gitignore", "w") as f:
25+
f.write("temp_test_repo/\n")
26+
subprocess.run(["git", "add", ".gitignore"], capture_output=True)
27+
subprocess.run(["git", "commit", "-m", "Initial commit"], capture_output=True)
28+
29+
def tearDown(self):
30+
os.chdir("..")
31+
if os.path.exists(self.test_dir):
32+
shutil.rmtree(self.test_dir)
33+
34+
def test_journal_mtime_validation(self):
35+
# Create a journal entry
36+
today = date.today().strftime("%Y-%m-%d")
37+
journal_path = f"journal/{today}.md"
38+
with open(journal_path, "w") as f:
39+
f.write("Journal entry")
40+
41+
# Ensure mtime is set
42+
time.sleep(0.1)
43+
44+
# Create a modified file
45+
with open("code.py", "w") as f:
46+
f.write("Code change")
47+
48+
# Hook should fail because code.py is newer than journal
49+
# We'll run the pre-commit hook script directly
50+
result = subprocess.run(["python3", "../.gemini/hooks/pre-commit.py"], capture_output=True, text=True)
51+
self.assertNotEqual(result.returncode, 0)
52+
self.assertIn("Updated journal required", result.stdout + result.stderr)
53+
54+
def test_journal_mtime_validation_success(self):
55+
# Add a dummy makefile to the test repo to avoid failure when running make test
56+
with open("makefile", "w") as f:
57+
f.write("test:\n\t@echo 'Tests passed'")
58+
59+
# Create a modified file
60+
with open("code.py", "w") as f:
61+
f.write("Code change")
62+
63+
# Create a journal entry AFTER the file modification
64+
time.sleep(0.1)
65+
today = date.today().strftime("%Y-%m-%d")
66+
journal_path = f"journal/{today}.md"
67+
with open(journal_path, "w") as f:
68+
f.write("Journal entry updated")
69+
70+
# Hook should succeed
71+
result = subprocess.run(["python3", "../.gemini/hooks/pre-commit.py"], capture_output=True, text=True)
72+
if result.returncode != 0:
73+
print(f"\nHOOK FAILED (rc={result.returncode})")
74+
print(f"STDOUT: {result.stdout}")
75+
print(f"STDERR: {result.stderr}")
76+
self.assertEqual(result.returncode, 0)
77+
self.assertIn("Validation passed", result.stdout)
78+
79+
def test_make_failure(self):
80+
# Add a failing makefile
81+
with open("makefile", "w") as f:
82+
f.write("test:\n\t@exit 1")
83+
84+
# Create a journal entry
85+
today = date.today().strftime("%Y-%m-%d")
86+
journal_path = f"journal/{today}.md"
87+
with open(journal_path, "w") as f:
88+
f.write("Journal entry")
89+
90+
# Ensure journal is newest
91+
time.sleep(0.1)
92+
os.utime(journal_path, None)
93+
94+
# Hook should fail because make test fails
95+
result = subprocess.run(["python3", "../.gemini/hooks/pre-commit.py"], capture_output=True, text=True)
96+
self.assertNotEqual(result.returncode, 0)
97+
self.assertIn("Validation failed", result.stdout)
98+
99+
def test_ignore_gemini_files(self):
100+
# Create a journal entry
101+
today = date.today().strftime("%Y-%m-%d")
102+
journal_path = f"journal/{today}.md"
103+
with open(journal_path, "w") as f:
104+
f.write("Journal entry")
105+
106+
time.sleep(0.1)
107+
108+
# Modify a .gemini file
109+
with open(".gemini/settings.json", "w") as f:
110+
f.write("{}")
111+
112+
# Hook should succeed because .gemini file is ignored in mtime check
113+
result = subprocess.run(["python3", "../.gemini/hooks/pre-commit.py"], capture_output=True, text=True)
114+
# It might fail because make test fails (no makefile)
115+
# But it shouldn't fail with "Updated journal required"
116+
self.assertNotIn("Updated journal required", result.stdout + result.stderr)
117+
118+
if __name__ == "__main__":
119+
unittest.main()

0 commit comments

Comments
 (0)