mirror of
https://github.com/safishamsi/graphify.git
synced 2026-09-14 01:26:07 +00:00
add git commit hook - auto-rebuilds graph after every commit
This commit is contained in:
@@ -70,6 +70,8 @@ When the user types `/graphify`, invoke the Skill tool with `skill: "graphify"`
|
||||
/graphify explain "SwinTransformer"
|
||||
|
||||
/graphify ./raw --watch # auto-sync graph as files change (code: instant, docs: notifies you)
|
||||
|
||||
graphify hook install # post-commit git hook - rebuilds graph on every commit automatically
|
||||
/graphify ./raw --wiki # build agent-crawlable wiki (index.md + article per community)
|
||||
/graphify ./raw --svg # export graph.svg
|
||||
/graphify ./raw --graphml # export graph.graphml (Gephi, yEd)
|
||||
@@ -98,6 +100,8 @@ Works with any mix of file types:
|
||||
|
||||
**Auto-sync** (`--watch`) - run in a background terminal and the graph updates itself as your codebase changes. Code file saves trigger an instant rebuild (AST only, no LLM). Doc/image changes notify you to run `--update` for the LLM re-pass. Useful for agentic workflows where multiple agents are writing code in parallel - the graph stays current between waves automatically.
|
||||
|
||||
**Git commit hook** (`graphify hook install`) - installs a post-commit hook that rebuilds the graph after every commit. No background process needed. Triggers once per commit, works with any editor, safe to add alongside existing hooks.
|
||||
|
||||
**Wiki** (`--wiki`) - Wikipedia-style markdown articles per community and god node, with an `index.md` entry point. Point any agent at `index.md` and it can navigate the knowledge base by reading files instead of parsing JSON.
|
||||
|
||||
Every edge is tagged `EXTRACTED`, `INFERRED`, or `AMBIGUOUS` - you always know what was found vs guessed.
|
||||
|
||||
@@ -59,12 +59,27 @@ def main() -> None:
|
||||
print("Commands:")
|
||||
print(" install copy skill to ~/.claude/skills/ and register in CLAUDE.md")
|
||||
print(" benchmark [graph.json] measure token reduction vs naive full-corpus approach")
|
||||
print(" hook install install post-commit git hook (auto-rebuilds graph on commit)")
|
||||
print(" hook uninstall remove post-commit git hook")
|
||||
print(" hook status check if hook is installed")
|
||||
print()
|
||||
return
|
||||
|
||||
cmd = sys.argv[1]
|
||||
if cmd == "install":
|
||||
install()
|
||||
elif cmd == "hook":
|
||||
from graphify.hooks import install as hook_install, uninstall as hook_uninstall, status as hook_status
|
||||
subcmd = sys.argv[2] if len(sys.argv) > 2 else ""
|
||||
if subcmd == "install":
|
||||
print(hook_install(Path(".")))
|
||||
elif subcmd == "uninstall":
|
||||
print(hook_uninstall(Path(".")))
|
||||
elif subcmd == "status":
|
||||
print(hook_status(Path(".")))
|
||||
else:
|
||||
print("Usage: graphify hook [install|uninstall|status]", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
elif cmd == "benchmark":
|
||||
from graphify.benchmark import run_benchmark, print_benchmark
|
||||
graph_path = sys.argv[2] if len(sys.argv) > 2 else "graphify-out/graph.json"
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
# git hook integration - install/uninstall graphify post-commit hook
|
||||
from __future__ import annotations
|
||||
from pathlib import Path
|
||||
|
||||
_HOOK_MARKER = "# graphify-hook"
|
||||
|
||||
_HOOK_SCRIPT = """\
|
||||
#!/bin/bash
|
||||
# graphify-hook
|
||||
# Auto-rebuilds the knowledge graph after each commit (code files only, no LLM needed).
|
||||
# Installed by: graphify hook install
|
||||
|
||||
CHANGED=$(git diff --name-only HEAD~1 HEAD 2>/dev/null || git diff --name-only HEAD 2>/dev/null)
|
||||
if [ -z "$CHANGED" ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
export GRAPHIFY_CHANGED="$CHANGED"
|
||||
python3 -c "
|
||||
import os, sys
|
||||
from pathlib import Path
|
||||
|
||||
CODE_EXTS = {
|
||||
'.py', '.ts', '.js', '.go', '.rs', '.java', '.cpp', '.c', '.rb', '.swift',
|
||||
'.kt', '.cs', '.scala', '.php', '.cc', '.cxx', '.hpp', '.h', '.kts',
|
||||
}
|
||||
|
||||
changed_raw = os.environ.get('GRAPHIFY_CHANGED', '')
|
||||
changed = [Path(f.strip()) for f in changed_raw.strip().splitlines() if f.strip()]
|
||||
code_changed = [f for f in changed if f.suffix.lower() in CODE_EXTS and f.exists()]
|
||||
|
||||
if not code_changed:
|
||||
sys.exit(0)
|
||||
|
||||
print(f'[graphify hook] {len(code_changed)} code file(s) changed - rebuilding graph...')
|
||||
|
||||
try:
|
||||
from graphify.watch import _rebuild_code
|
||||
_rebuild_code(Path('.'))
|
||||
except Exception as exc:
|
||||
print(f'[graphify hook] Rebuild failed: {exc}')
|
||||
sys.exit(0)
|
||||
"
|
||||
"""
|
||||
|
||||
|
||||
def _git_root(path: Path) -> Path | None:
|
||||
"""Walk up to find .git directory."""
|
||||
current = path.resolve()
|
||||
for parent in [current, *current.parents]:
|
||||
if (parent / ".git").exists():
|
||||
return parent
|
||||
return None
|
||||
|
||||
|
||||
def install(path: Path = Path(".")) -> str:
|
||||
"""Install graphify post-commit hook in the nearest git repo.
|
||||
|
||||
Returns a message describing what was done.
|
||||
"""
|
||||
root = _git_root(path)
|
||||
if root is None:
|
||||
raise RuntimeError(f"No git repository found at or above {path.resolve()}")
|
||||
|
||||
hooks_dir = root / ".git" / "hooks"
|
||||
hooks_dir.mkdir(exist_ok=True)
|
||||
hook_path = hooks_dir / "post-commit"
|
||||
|
||||
if hook_path.exists():
|
||||
content = hook_path.read_text()
|
||||
if _HOOK_MARKER in content:
|
||||
return f"graphify hook already installed at {hook_path}"
|
||||
# Append to existing hook
|
||||
hook_path.write_text(content.rstrip() + "\n\n" + _HOOK_SCRIPT)
|
||||
return f"graphify hook appended to existing post-commit hook at {hook_path}"
|
||||
|
||||
hook_path.write_text(_HOOK_SCRIPT)
|
||||
hook_path.chmod(0o755)
|
||||
return f"graphify hook installed at {hook_path}"
|
||||
|
||||
|
||||
def uninstall(path: Path = Path(".")) -> str:
|
||||
"""Remove graphify post-commit hook."""
|
||||
root = _git_root(path)
|
||||
if root is None:
|
||||
raise RuntimeError(f"No git repository found at or above {path.resolve()}")
|
||||
|
||||
hook_path = root / ".git" / "hooks" / "post-commit"
|
||||
if not hook_path.exists():
|
||||
return "No post-commit hook found - nothing to remove."
|
||||
|
||||
content = hook_path.read_text()
|
||||
if _HOOK_MARKER not in content:
|
||||
return "graphify hook not found in post-commit - nothing to remove."
|
||||
|
||||
# Strip everything from our marker onwards
|
||||
before = content.split(_HOOK_MARKER)[0].rstrip()
|
||||
# 'before' is empty or just a shebang line if the whole file was ours
|
||||
non_empty = [l for l in before.splitlines() if l.strip() and not l.startswith("#!")]
|
||||
if not non_empty:
|
||||
hook_path.unlink()
|
||||
return f"Removed post-commit hook at {hook_path}"
|
||||
else:
|
||||
hook_path.write_text(before + "\n")
|
||||
return f"graphify hook removed from {hook_path} (other hook content preserved)"
|
||||
|
||||
|
||||
def status(path: Path = Path(".")) -> str:
|
||||
"""Check if graphify hook is installed."""
|
||||
root = _git_root(path)
|
||||
if root is None:
|
||||
return "Not in a git repository."
|
||||
hook_path = root / ".git" / "hooks" / "post-commit"
|
||||
if not hook_path.exists():
|
||||
return "graphify hook: not installed"
|
||||
if _HOOK_MARKER in hook_path.read_text():
|
||||
return f"graphify hook: installed at {hook_path}"
|
||||
return "graphify hook: not installed (post-commit exists but graphify hook not found)"
|
||||
@@ -1119,6 +1119,22 @@ For agentic workflows: run `--watch` in a background terminal. Code changes from
|
||||
|
||||
---
|
||||
|
||||
## For git commit hook
|
||||
|
||||
Install a post-commit hook that auto-rebuilds the graph after every commit. No background process needed - triggers once per commit, works with any editor.
|
||||
|
||||
```bash
|
||||
graphify hook install # install
|
||||
graphify hook uninstall # remove
|
||||
graphify hook status # check
|
||||
```
|
||||
|
||||
After every `git commit`, the hook detects which code files changed (via `git diff HEAD~1`), re-runs AST extraction on those files, and rebuilds `graph.json` and `GRAPH_REPORT.md`. Doc/image changes are ignored by the hook - run `/graphify --update` manually for those.
|
||||
|
||||
If a post-commit hook already exists, graphify appends to it rather than replacing it.
|
||||
|
||||
---
|
||||
|
||||
## Honesty Rules
|
||||
|
||||
- Never invent an edge. If unsure, use AMBIGUOUS.
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "graphifyy"
|
||||
version = "0.1.9"
|
||||
version = "0.1.10"
|
||||
description = "Claude Code skill - turn any folder of code, docs, papers, images, or tweets into a queryable knowledge graph"
|
||||
readme = "README.md"
|
||||
license = { text = "MIT" }
|
||||
|
||||
@@ -1119,6 +1119,22 @@ For agentic workflows: run `--watch` in a background terminal. Code changes from
|
||||
|
||||
---
|
||||
|
||||
## For git commit hook
|
||||
|
||||
Install a post-commit hook that auto-rebuilds the graph after every commit. No background process needed - triggers once per commit, works with any editor.
|
||||
|
||||
```bash
|
||||
graphify hook install # install
|
||||
graphify hook uninstall # remove
|
||||
graphify hook status # check
|
||||
```
|
||||
|
||||
After every `git commit`, the hook detects which code files changed (via `git diff HEAD~1`), re-runs AST extraction on those files, and rebuilds `graph.json` and `GRAPH_REPORT.md`. Doc/image changes are ignored by the hook - run `/graphify --update` manually for those.
|
||||
|
||||
If a post-commit hook already exists, graphify appends to it rather than replacing it.
|
||||
|
||||
---
|
||||
|
||||
## Honesty Rules
|
||||
|
||||
- Never invent an edge. If unsure, use AMBIGUOUS.
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
"""Tests for hooks.py - git hook install/uninstall."""
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
import pytest
|
||||
from graphify.hooks import install, uninstall, status, _HOOK_MARKER
|
||||
|
||||
|
||||
def _make_git_repo(tmp_path: Path) -> Path:
|
||||
subprocess.run(["git", "init", str(tmp_path)], check=True, capture_output=True)
|
||||
return tmp_path
|
||||
|
||||
|
||||
def test_install_creates_hook(tmp_path):
|
||||
repo = _make_git_repo(tmp_path)
|
||||
result = install(repo)
|
||||
hook = repo / ".git" / "hooks" / "post-commit"
|
||||
assert hook.exists()
|
||||
assert _HOOK_MARKER in hook.read_text()
|
||||
assert "installed" in result
|
||||
|
||||
|
||||
def test_install_is_executable(tmp_path):
|
||||
repo = _make_git_repo(tmp_path)
|
||||
install(repo)
|
||||
hook = repo / ".git" / "hooks" / "post-commit"
|
||||
assert hook.stat().st_mode & 0o111 # executable bit set
|
||||
|
||||
|
||||
def test_install_idempotent(tmp_path):
|
||||
repo = _make_git_repo(tmp_path)
|
||||
install(repo)
|
||||
result = install(repo)
|
||||
assert "already installed" in result
|
||||
# marker appears only once
|
||||
hook = repo / ".git" / "hooks" / "post-commit"
|
||||
assert hook.read_text().count(_HOOK_MARKER) == 1
|
||||
|
||||
|
||||
def test_install_appends_to_existing_hook(tmp_path):
|
||||
repo = _make_git_repo(tmp_path)
|
||||
hook = repo / ".git" / "hooks" / "post-commit"
|
||||
hook.write_text("#!/bin/bash\necho existing\n")
|
||||
hook.chmod(0o755)
|
||||
install(repo)
|
||||
content = hook.read_text()
|
||||
assert "existing" in content
|
||||
assert _HOOK_MARKER in content
|
||||
|
||||
|
||||
def test_uninstall_removes_hook(tmp_path):
|
||||
repo = _make_git_repo(tmp_path)
|
||||
install(repo)
|
||||
result = uninstall(repo)
|
||||
hook = repo / ".git" / "hooks" / "post-commit"
|
||||
assert not hook.exists()
|
||||
assert "Removed" in result
|
||||
|
||||
|
||||
def test_uninstall_no_hook(tmp_path):
|
||||
repo = _make_git_repo(tmp_path)
|
||||
result = uninstall(repo)
|
||||
assert "nothing to remove" in result
|
||||
|
||||
|
||||
def test_status_installed(tmp_path):
|
||||
repo = _make_git_repo(tmp_path)
|
||||
install(repo)
|
||||
result = status(repo)
|
||||
assert "installed" in result
|
||||
|
||||
|
||||
def test_status_not_installed(tmp_path):
|
||||
repo = _make_git_repo(tmp_path)
|
||||
result = status(repo)
|
||||
assert "not installed" in result
|
||||
|
||||
|
||||
def test_no_git_repo_raises(tmp_path):
|
||||
with pytest.raises(RuntimeError, match="No git repository"):
|
||||
install(tmp_path / "not_a_repo")
|
||||
Reference in New Issue
Block a user