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 +21 -0
- git_sage-0.1.0/PKG-INFO +176 -0
- git_sage-0.1.0/README.md +162 -0
- git_sage-0.1.0/git_sage/__init__.py +3 -0
- git_sage-0.1.0/git_sage/cli.py +222 -0
- git_sage-0.1.0/git_sage/diff.py +76 -0
- git_sage-0.1.0/git_sage/hook.py +128 -0
- git_sage-0.1.0/git_sage/ollama.py +123 -0
- git_sage-0.1.0/git_sage/output.py +130 -0
- git_sage-0.1.0/git_sage/parser.py +148 -0
- git_sage-0.1.0/git_sage/prompt.py +100 -0
- git_sage-0.1.0/git_sage.egg-info/PKG-INFO +176 -0
- git_sage-0.1.0/git_sage.egg-info/SOURCES.txt +20 -0
- git_sage-0.1.0/git_sage.egg-info/dependency_links.txt +1 -0
- git_sage-0.1.0/git_sage.egg-info/entry_points.txt +2 -0
- git_sage-0.1.0/git_sage.egg-info/requires.txt +3 -0
- git_sage-0.1.0/git_sage.egg-info/top_level.txt +1 -0
- git_sage-0.1.0/pyproject.toml +27 -0
- git_sage-0.1.0/setup.cfg +4 -0
- git_sage-0.1.0/tests/test_diff.py +58 -0
- git_sage-0.1.0/tests/test_parser.py +145 -0
- git_sage-0.1.0/tests/test_prompt.py +52 -0
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.
|
git_sage-0.1.0/PKG-INFO
ADDED
|
@@ -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
|
git_sage-0.1.0/README.md
ADDED
|
@@ -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,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
|