git-sage 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
git_sage-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Joel Adewole
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,176 @@
1
+ Metadata-Version: 2.4
2
+ Name: git-sage
3
+ Version: 0.1.0
4
+ Summary: Local AI code reviewer for your git workflow. Powered by Ollama
5
+ Author-email: Joel Adewole <joeladewole3@gmail.com>
6
+ License: MIT
7
+ Requires-Python: >=3.9
8
+ Description-Content-Type: text/markdown
9
+ License-File: LICENSE
10
+ Requires-Dist: click>=8.1
11
+ Requires-Dist: httpx>=0.27
12
+ Requires-Dist: rich>=13.0
13
+ Dynamic: license-file
14
+
15
+ # git-sage
16
+
17
+ > Local AI code review right before you push. No cloud. No subscriptions. No data leaving your machine.
18
+
19
+ `git-sage` hooks into your git workflow and runs a code review using a locally hosted LLM via [Ollama](https://ollama.com). When you run `git push`, the tool intercepts it, sends your staged diff to the model, and either approves the push or asks you to revise, all on your machine, in seconds.
20
+ ```
21
+ $ git push
22
+
23
+ Staged: 3 file(s) +47 / -12
24
+
25
+ ╭─ Summary ───────────────────────────────────────────────────────────╮
26
+ │ Adds a /login endpoint with bcrypt password hashing. │
27
+ ╰─────────────────────────────────────────────────────────────────────╯
28
+
29
+ Issues (2 found)
30
+
31
+ ● 1. The SECRET_KEY is hardcoded as a string literal on line 14.
32
+ ● 2. There is no rate limiting on the /login route.
33
+
34
+ Suggestions (1)
35
+
36
+ ◆ 1. Load SECRET_KEY from os.getenv('SECRET_KEY') instead.
37
+
38
+ ╭─────────────────────────────────────────────────────────────────────╮
39
+ │ ✗ REVISE │
40
+ │ Address the issues above before pushing. │
41
+ ╰─────────────────────────────────────────────────────────────────────╯
42
+
43
+ Push aborted by git-sage. Fix the issues above, or run:
44
+ git push --no-verify to bypass the hook.
45
+ ```
46
+
47
+ 📖 **[Full documentation →](https://wolz-codelife.github.io/git-sage/)**
48
+
49
+ ---
50
+
51
+ ## Why git-sage?
52
+
53
+ Most AI code review tools sit at the pull request stage, by then your code has already reached a remote server. A hardcoded secret has already been pushed. A vulnerable dependency is already on a branch other developers may have pulled.
54
+
55
+ `git-sage` moves the review to your local machine, before any code leaves it. If the model finds a problem, the push is aborted and you fix it right there in your editor.
56
+
57
+ ---
58
+
59
+ ## Requirements
60
+
61
+ - Python 3.9+
62
+ - [Ollama](https://ollama.com) installed and running
63
+ - macOS, Linux, or Windows (WSL2)
64
+ - ~5 GB disk space for the default model
65
+
66
+ No GPU required. Runs on any modern laptop.
67
+
68
+ ---
69
+
70
+ ## Quick start
71
+
72
+ **1. Install Ollama and pull the model**
73
+ ```bash
74
+ brew install ollama # macOS — see docs for Linux/Windows
75
+ ollama serve
76
+ ollama pull qwen2.5-coder:7b
77
+ ```
78
+
79
+ **2. Install git-sage**
80
+ ```bash
81
+ pip install git-sage
82
+ ```
83
+
84
+ **3. Install the hook in your repo**
85
+ ```bash
86
+ cd your-project
87
+ git-sage install
88
+ ```
89
+
90
+ **4. Push as normal**
91
+ ```bash
92
+ git push # review runs automatically
93
+ ```
94
+
95
+ ---
96
+
97
+ ## Commands
98
+
99
+ | Command | Description |
100
+ |----------------------------------------------------|-------------------------------------------|
101
+ | `git-sage review` | Manually review staged changes |
102
+ | `git-sage review --model llama3.2` | Use a different local model |
103
+ | `git-sage review --context "Adds OAuth"` | Provide context to the model |
104
+ | `git-sage review --diff-mode head` | Review the last commit instead |
105
+ | `git-sage review --diff-mode branch --base main` | Review the whole branch |
106
+ | `git-sage review --force` | Review but don't abort push on REVISE |
107
+ | `git-sage install` | Install the pre-push hook |
108
+ | `git-sage uninstall` | Remove the pre-push hook |
109
+ | `git-sage status` | Check Ollama availability and hook status |
110
+ | `git-sage models` | List locally available Ollama models |
111
+
112
+ ---
113
+
114
+ ## How it works
115
+ ```
116
+ git push
117
+ → .git/hooks/pre-push fires
118
+ → git-sage review --hook
119
+ → git diff --cached (extract the staged diff)
120
+ → build prompt (diff + system instructions)
121
+ → POST localhost:11434 (Ollama local API)
122
+ → parse response (SUMMARY / ISSUES / SUGGESTIONS / VERDICT)
123
+ → render to terminal (rich coloured output)
124
+ → exit 0 (APPROVE) or exit 1 (REVISE, aborts push)
125
+ ```
126
+
127
+ For a full breakdown of the architecture and each module, see the **[Architecture docs](https://wolz-codelife.github.io/git-sage/docs/architecture)**.
128
+
129
+ ---
130
+
131
+ ## Bypassing the hook
132
+ ```bash
133
+ git push --no-verify
134
+ ```
135
+
136
+ ---
137
+
138
+ ## Project structure
139
+ ```
140
+ git_sage/
141
+ cli.py CLI entrypoint (click)
142
+ diff.py Git diff extraction
143
+ prompt.py Prompt builder
144
+ ollama.py Ollama HTTP client
145
+ parser.py Response parser
146
+ output.py Terminal renderer (rich)
147
+ hook.py Git hook installer
148
+ tests/
149
+ test_parser.py
150
+ test_diff.py
151
+ test_prompt.py
152
+ docs/ Docusaurus documentation site
153
+ CHANGELOG.md Version history
154
+ ```
155
+
156
+ ---
157
+
158
+ ## Running tests
159
+ ```bash
160
+ pip install pytest
161
+ pytest tests/ -v
162
+ ```
163
+
164
+ Tests are self-contained; no Ollama or git repo needed.
165
+
166
+ ---
167
+
168
+ ## Contributing
169
+
170
+ Contributions are welcome. See the **[Contributing guide](https://wolz-codelife.github.io/git-sage/docs/contributing)** for how to get started, issue templates, and a PR template.
171
+
172
+ ---
173
+
174
+ ## License
175
+
176
+ MIT
@@ -0,0 +1,162 @@
1
+ # git-sage
2
+
3
+ > Local AI code review right before you push. No cloud. No subscriptions. No data leaving your machine.
4
+
5
+ `git-sage` hooks into your git workflow and runs a code review using a locally hosted LLM via [Ollama](https://ollama.com). When you run `git push`, the tool intercepts it, sends your staged diff to the model, and either approves the push or asks you to revise, all on your machine, in seconds.
6
+ ```
7
+ $ git push
8
+
9
+ Staged: 3 file(s) +47 / -12
10
+
11
+ ╭─ Summary ───────────────────────────────────────────────────────────╮
12
+ │ Adds a /login endpoint with bcrypt password hashing. │
13
+ ╰─────────────────────────────────────────────────────────────────────╯
14
+
15
+ Issues (2 found)
16
+
17
+ ● 1. The SECRET_KEY is hardcoded as a string literal on line 14.
18
+ ● 2. There is no rate limiting on the /login route.
19
+
20
+ Suggestions (1)
21
+
22
+ ◆ 1. Load SECRET_KEY from os.getenv('SECRET_KEY') instead.
23
+
24
+ ╭─────────────────────────────────────────────────────────────────────╮
25
+ │ ✗ REVISE │
26
+ │ Address the issues above before pushing. │
27
+ ╰─────────────────────────────────────────────────────────────────────╯
28
+
29
+ Push aborted by git-sage. Fix the issues above, or run:
30
+ git push --no-verify to bypass the hook.
31
+ ```
32
+
33
+ 📖 **[Full documentation →](https://wolz-codelife.github.io/git-sage/)**
34
+
35
+ ---
36
+
37
+ ## Why git-sage?
38
+
39
+ Most AI code review tools sit at the pull request stage, by then your code has already reached a remote server. A hardcoded secret has already been pushed. A vulnerable dependency is already on a branch other developers may have pulled.
40
+
41
+ `git-sage` moves the review to your local machine, before any code leaves it. If the model finds a problem, the push is aborted and you fix it right there in your editor.
42
+
43
+ ---
44
+
45
+ ## Requirements
46
+
47
+ - Python 3.9+
48
+ - [Ollama](https://ollama.com) installed and running
49
+ - macOS, Linux, or Windows (WSL2)
50
+ - ~5 GB disk space for the default model
51
+
52
+ No GPU required. Runs on any modern laptop.
53
+
54
+ ---
55
+
56
+ ## Quick start
57
+
58
+ **1. Install Ollama and pull the model**
59
+ ```bash
60
+ brew install ollama # macOS — see docs for Linux/Windows
61
+ ollama serve
62
+ ollama pull qwen2.5-coder:7b
63
+ ```
64
+
65
+ **2. Install git-sage**
66
+ ```bash
67
+ pip install git-sage
68
+ ```
69
+
70
+ **3. Install the hook in your repo**
71
+ ```bash
72
+ cd your-project
73
+ git-sage install
74
+ ```
75
+
76
+ **4. Push as normal**
77
+ ```bash
78
+ git push # review runs automatically
79
+ ```
80
+
81
+ ---
82
+
83
+ ## Commands
84
+
85
+ | Command | Description |
86
+ |----------------------------------------------------|-------------------------------------------|
87
+ | `git-sage review` | Manually review staged changes |
88
+ | `git-sage review --model llama3.2` | Use a different local model |
89
+ | `git-sage review --context "Adds OAuth"` | Provide context to the model |
90
+ | `git-sage review --diff-mode head` | Review the last commit instead |
91
+ | `git-sage review --diff-mode branch --base main` | Review the whole branch |
92
+ | `git-sage review --force` | Review but don't abort push on REVISE |
93
+ | `git-sage install` | Install the pre-push hook |
94
+ | `git-sage uninstall` | Remove the pre-push hook |
95
+ | `git-sage status` | Check Ollama availability and hook status |
96
+ | `git-sage models` | List locally available Ollama models |
97
+
98
+ ---
99
+
100
+ ## How it works
101
+ ```
102
+ git push
103
+ → .git/hooks/pre-push fires
104
+ → git-sage review --hook
105
+ → git diff --cached (extract the staged diff)
106
+ → build prompt (diff + system instructions)
107
+ → POST localhost:11434 (Ollama local API)
108
+ → parse response (SUMMARY / ISSUES / SUGGESTIONS / VERDICT)
109
+ → render to terminal (rich coloured output)
110
+ → exit 0 (APPROVE) or exit 1 (REVISE, aborts push)
111
+ ```
112
+
113
+ For a full breakdown of the architecture and each module, see the **[Architecture docs](https://wolz-codelife.github.io/git-sage/docs/architecture)**.
114
+
115
+ ---
116
+
117
+ ## Bypassing the hook
118
+ ```bash
119
+ git push --no-verify
120
+ ```
121
+
122
+ ---
123
+
124
+ ## Project structure
125
+ ```
126
+ git_sage/
127
+ cli.py CLI entrypoint (click)
128
+ diff.py Git diff extraction
129
+ prompt.py Prompt builder
130
+ ollama.py Ollama HTTP client
131
+ parser.py Response parser
132
+ output.py Terminal renderer (rich)
133
+ hook.py Git hook installer
134
+ tests/
135
+ test_parser.py
136
+ test_diff.py
137
+ test_prompt.py
138
+ docs/ Docusaurus documentation site
139
+ CHANGELOG.md Version history
140
+ ```
141
+
142
+ ---
143
+
144
+ ## Running tests
145
+ ```bash
146
+ pip install pytest
147
+ pytest tests/ -v
148
+ ```
149
+
150
+ Tests are self-contained; no Ollama or git repo needed.
151
+
152
+ ---
153
+
154
+ ## Contributing
155
+
156
+ Contributions are welcome. See the **[Contributing guide](https://wolz-codelife.github.io/git-sage/docs/contributing)** for how to get started, issue templates, and a PR template.
157
+
158
+ ---
159
+
160
+ ## License
161
+
162
+ MIT
@@ -0,0 +1,3 @@
1
+ """git-sage: local AI code review for your git workflow."""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,222 @@
1
+ """
2
+ cli.py
3
+ ------
4
+ Click-based CLI entrypoint for git-sage.
5
+
6
+ Commands
7
+ --------
8
+ git-sage review Run a review of staged changes (interactive)
9
+ git-sage review --hook Run a review triggered by the pre-push hook
10
+ git-sage install Install the pre-push hook in the current repo
11
+ git-sage uninstall Remove the pre-push hook
12
+ git-sage status Show tool version, hook status, and Ollama availability
13
+ git-sage models List locally available Ollama models
14
+ """
15
+
16
+ import sys
17
+ import click
18
+
19
+ from git_sage import __version__
20
+ from git_sage import diff as diff_mod
21
+ from git_sage import hook as hook_mod
22
+ from git_sage import ollama as ollama_mod
23
+ from git_sage import output
24
+ from git_sage.prompt import build_messages
25
+ from git_sage.parser import parse, Verdict
26
+
27
+
28
+ # ---------------------------------------------------------------------------
29
+ # Root group
30
+ # ---------------------------------------------------------------------------
31
+
32
+ @click.group()
33
+ @click.version_option(__version__, prog_name="git-sage")
34
+ def main() -> None:
35
+ """git-sage — local AI code review for your git workflow."""
36
+
37
+
38
+ # ---------------------------------------------------------------------------
39
+ # review
40
+ # ---------------------------------------------------------------------------
41
+
42
+ @main.command()
43
+ @click.option(
44
+ "--model", "-m",
45
+ default=ollama_mod.DEFAULT_MODEL,
46
+ show_default=True,
47
+ help="Ollama model to use for review.",
48
+ )
49
+ @click.option(
50
+ "--host",
51
+ default=ollama_mod.DEFAULT_HOST,
52
+ show_default=True,
53
+ help="Ollama server URL.",
54
+ )
55
+ @click.option(
56
+ "--context", "-c",
57
+ default=None,
58
+ help='Optional note about this change, e.g. "Adds OAuth login".',
59
+ )
60
+ @click.option(
61
+ "--hook",
62
+ is_flag=True,
63
+ hidden=True,
64
+ help="Internal flag: invoked from the pre-push hook.",
65
+ )
66
+ @click.option(
67
+ "--diff-mode",
68
+ type=click.Choice(["staged", "head", "branch"]),
69
+ default="staged",
70
+ show_default=True,
71
+ help="Which diff to review.",
72
+ )
73
+ @click.option(
74
+ "--base",
75
+ default="main",
76
+ show_default=True,
77
+ help="Base branch for --diff-mode=branch.",
78
+ )
79
+ @click.option(
80
+ "--force", "-f",
81
+ is_flag=True,
82
+ help="Do not abort the push even if the verdict is REVISE (hook mode only).",
83
+ )
84
+ def review(model, host, context, hook, diff_mode, base, force) -> None:
85
+ """Review staged (or recent) changes with a local AI model."""
86
+
87
+ # 1. Check Ollama is running
88
+ if not ollama_mod.is_available(host):
89
+ output.print_error(
90
+ f"Ollama is not running at {host}.\n"
91
+ " Start it with: ollama serve\n"
92
+ f" Then pull a model: ollama pull {ollama_mod.DEFAULT_MODEL}"
93
+ )
94
+ sys.exit(1)
95
+
96
+ # 2. Extract the diff
97
+ try:
98
+ if diff_mode == "staged":
99
+ diff = diff_mod.get_staged_diff()
100
+ elif diff_mode == "head":
101
+ diff = diff_mod.get_head_diff()
102
+ else:
103
+ diff = diff_mod.get_branch_diff(base)
104
+ except RuntimeError as exc:
105
+ output.print_error(str(exc))
106
+ sys.exit(1)
107
+
108
+ if not diff.raw.strip():
109
+ output.print_warning("No changes found to review.")
110
+ sys.exit(0)
111
+
112
+ output.print_diff_stats(diff)
113
+
114
+ # 3. Build prompt and call Ollama
115
+ messages = build_messages(diff, context)
116
+
117
+ try:
118
+ with output.thinking_spinner(f"Reviewing with {model}…"):
119
+ raw_response = ollama_mod.chat(messages, model=model, host=host)
120
+ except ollama_mod.OllamaError as exc:
121
+ output.print_error(f"Ollama error: {exc}")
122
+ sys.exit(1)
123
+
124
+ # 4. Parse and render
125
+ result = parse(raw_response)
126
+ output.print_review(result)
127
+
128
+ # 5. Hook mode: non-zero exit aborts the push
129
+ if hook and result.verdict == Verdict.REVISE and not force:
130
+ click.echo(
131
+ " Push aborted by git-sage. Fix the issues above, or run:\n"
132
+ " git push --no-verify to bypass the hook.\n"
133
+ )
134
+ sys.exit(1)
135
+
136
+
137
+ # ---------------------------------------------------------------------------
138
+ # install
139
+ # ---------------------------------------------------------------------------
140
+
141
+ @main.command()
142
+ def install() -> None:
143
+ """Install the git-sage pre-push hook in the current repository."""
144
+ try:
145
+ hook_path = hook_mod.install()
146
+ output.print_success(f"Hook installed at {hook_path}")
147
+ click.echo(" git-sage will now review your changes before every push.\n")
148
+ except RuntimeError as exc:
149
+ output.print_error(str(exc))
150
+ sys.exit(1)
151
+
152
+
153
+ # ---------------------------------------------------------------------------
154
+ # uninstall
155
+ # ---------------------------------------------------------------------------
156
+
157
+ @main.command()
158
+ def uninstall() -> None:
159
+ """Remove the git-sage pre-push hook from the current repository."""
160
+ try:
161
+ removed = hook_mod.uninstall()
162
+ if removed:
163
+ output.print_success("Hook removed.")
164
+ else:
165
+ output.print_warning("No git-sage hook found in this repository.")
166
+ except RuntimeError as exc:
167
+ output.print_error(str(exc))
168
+ sys.exit(1)
169
+
170
+
171
+ # ---------------------------------------------------------------------------
172
+ # status
173
+ # ---------------------------------------------------------------------------
174
+
175
+ @main.command()
176
+ @click.option("--host", default=ollama_mod.DEFAULT_HOST, show_default=True)
177
+ def status(host) -> None:
178
+ """Show the current status of git-sage, the hook, and Ollama."""
179
+ click.echo(f"\n git-sage v{__version__}\n")
180
+
181
+ # Ollama
182
+ if ollama_mod.is_available(host):
183
+ click.echo(f" [✓] Ollama running at {host}")
184
+ models = ollama_mod.list_models(host)
185
+ if models:
186
+ click.echo(f" Models: {', '.join(models)}")
187
+ else:
188
+ click.echo(f" [✗] Ollama not reachable at {host}")
189
+ click.echo( " Start with: ollama serve")
190
+
191
+ # Hook
192
+ if hook_mod.is_installed():
193
+ click.echo(" [✓] pre-push hook installed")
194
+ else:
195
+ click.echo(" [ ] pre-push hook not installed")
196
+ click.echo(" Run: git-sage install")
197
+
198
+ click.echo()
199
+
200
+
201
+ # ---------------------------------------------------------------------------
202
+ # models
203
+ # ---------------------------------------------------------------------------
204
+
205
+ @main.command()
206
+ @click.option("--host", default=ollama_mod.DEFAULT_HOST, show_default=True)
207
+ def models(host) -> None:
208
+ """List locally available Ollama models."""
209
+ if not ollama_mod.is_available(host):
210
+ output.print_error(f"Ollama is not running at {host}.")
211
+ sys.exit(1)
212
+
213
+ model_list = ollama_mod.list_models(host)
214
+ if not model_list:
215
+ click.echo("\n No models found. Pull one with:\n")
216
+ click.echo(f" ollama pull {ollama_mod.DEFAULT_MODEL}\n")
217
+ else:
218
+ click.echo(f"\n Available models ({len(model_list)}):\n")
219
+ for m in model_list:
220
+ marker = " ●" if m.startswith("qwen2.5-coder") else " ○"
221
+ click.echo(f"{marker} {m}")
222
+ click.echo()
@@ -0,0 +1,76 @@
1
+ """
2
+ diff.py
3
+ -------
4
+ Extracts diffs from git using subprocess.
5
+
6
+ Supports two modes:
7
+ - staged: changes added with `git add` (used during pre-push / manual review)
8
+ - head: diff of the last commit vs its parent (useful for post-commit review)
9
+ """
10
+
11
+ import subprocess
12
+ from dataclasses import dataclass
13
+
14
+
15
+ @dataclass
16
+ class DiffResult:
17
+ raw: str # full unified diff text
18
+ file_count: int # number of changed files
19
+ additions: int # total lines added
20
+ deletions: int # total lines removed
21
+ files: list[str] # list of changed file paths
22
+
23
+
24
+ def get_staged_diff() -> DiffResult:
25
+ """Return the diff of all staged changes (git diff --cached)."""
26
+ return _run_diff(["git", "diff", "--cached"])
27
+
28
+
29
+ def get_head_diff() -> DiffResult:
30
+ """Return the diff of the last commit vs its parent (git diff HEAD~1 HEAD)."""
31
+ return _run_diff(["git", "diff", "HEAD~1", "HEAD"])
32
+
33
+
34
+ def get_branch_diff(base: str = "main") -> DiffResult:
35
+ """Return the diff of the current branch vs a base branch."""
36
+ return _run_diff(["git", "diff", f"{base}...HEAD"])
37
+
38
+
39
+ def _run_diff(cmd: list[str]) -> DiffResult:
40
+ result = subprocess.run(
41
+ cmd,
42
+ capture_output=True,
43
+ text=True,
44
+ )
45
+
46
+ if result.returncode != 0:
47
+ raise RuntimeError(
48
+ f"git diff failed:\n{result.stderr.strip()}"
49
+ )
50
+
51
+ raw = result.stdout
52
+
53
+ if not raw.strip():
54
+ return DiffResult(raw="", file_count=0, additions=0, deletions=0, files=[])
55
+
56
+ additions = sum(1 for line in raw.splitlines() if line.startswith("+") and not line.startswith("+++"))
57
+ deletions = sum(1 for line in raw.splitlines() if line.startswith("-") and not line.startswith("---"))
58
+ files = _extract_files(raw)
59
+
60
+ return DiffResult(
61
+ raw=raw,
62
+ file_count=len(files),
63
+ additions=additions,
64
+ deletions=deletions,
65
+ files=files,
66
+ )
67
+
68
+
69
+ def _extract_files(diff_text: str) -> list[str]:
70
+ files = []
71
+ for line in diff_text.splitlines():
72
+ if line.startswith("+++ b/"):
73
+ path = line.removeprefix("+++ b/")
74
+ if path not in files:
75
+ files.append(path)
76
+ return files