gitai-cli 0.1.0__py3-none-any.whl

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.
gitai/__init__.py ADDED
@@ -0,0 +1 @@
1
+ __version__ = "0.1.0"
gitai/ai.py ADDED
@@ -0,0 +1,52 @@
1
+ import litellm
2
+ from litellm.exceptions import APIConnectionError, AuthenticationError
3
+ from gitai.config import load_config
4
+
5
+ def get_commit_suggestions(prompt: str) -> list[str]:
6
+ config = load_config()
7
+
8
+ provider = config["provider"].lower()
9
+ model = config["model"]
10
+ model_string = f"{provider}/{model}"
11
+
12
+ kwargs = {
13
+ "model": model_string,
14
+ "messages": [{"role": "user", "content": prompt}],
15
+ }
16
+
17
+ if provider == "ollama":
18
+ kwargs["api_base"] = config["ollama_url"]
19
+
20
+ try:
21
+ response = litellm.completion(**kwargs)
22
+ except APIConnectionError:
23
+ if provider == "ollama":
24
+ raise SystemExit(
25
+ f"[gitai] Could not connect to Ollama at {config['ollama_url']}. "
26
+ "Is Ollama running? Try: ollama serve"
27
+ )
28
+ raise SystemExit(
29
+ f"[gitai] Could not connect to the {provider} API. "
30
+ "Check your network connection and try again."
31
+ )
32
+ except AuthenticationError:
33
+ key_map = {
34
+ "openai": "OPENAI_API_KEY",
35
+ "anthropic": "ANTHROPIC_API_KEY",
36
+ "gemini": "GEMINI_API_KEY",
37
+ }
38
+ env_var = key_map.get(provider, f"{provider.upper()}_API_KEY")
39
+ raise SystemExit(
40
+ f"[gitai] Authentication failed for {provider}. "
41
+ f"Set the {env_var} environment variable and try again."
42
+ )
43
+
44
+ text = response.choices[0].message.content.strip()
45
+
46
+ suggestions = []
47
+ for line in text.splitlines():
48
+ line = line.strip()
49
+ if line and line[0].isdigit() and "." in line:
50
+ suggestions.append(line)
51
+
52
+ return suggestions
gitai/cli.py ADDED
@@ -0,0 +1,132 @@
1
+ from typing import Optional
2
+ import typer
3
+ import questionary
4
+ import subprocess
5
+ from gitai.config import load_config, save_config
6
+ from gitai.git import get_staged_diff, get_repo_name, is_diff_meaningful
7
+ from gitai.prompt import build_commit_prompt
8
+ from gitai.ai import get_commit_suggestions
9
+ from gitai import __version__
10
+
11
+ VALID_PROVIDERS = {"ollama", "openai", "anthropic", "gemini"}
12
+ VALID_COMMIT_STYLES = {"conventional", "free-form"}
13
+
14
+ app = typer.Typer()
15
+
16
+
17
+ def _version_callback(value: bool):
18
+ if value:
19
+ typer.echo(f"gitai {__version__}")
20
+ raise typer.Exit()
21
+
22
+
23
+ @app.callback()
24
+ def main(
25
+ version: Optional[bool] = typer.Option(
26
+ None, "--version", "-V",
27
+ callback=_version_callback,
28
+ is_eager=True,
29
+ help="Show version and exit.",
30
+ ),
31
+ ):
32
+ """AI-powered git commit message generator."""
33
+
34
+
35
+ @app.command()
36
+ def commit():
37
+ """Generate AI-powered commit message suggestions for staged changes."""
38
+ typer.echo("🔍 Reading your git diff...")
39
+
40
+ diff = get_staged_diff()
41
+ if not diff:
42
+ typer.echo("No staged changes found. Please stage your changes before committing.")
43
+ raise typer.Exit(code=1)
44
+
45
+ if not is_diff_meaningful(diff):
46
+ typer.echo("Staged file appears to be empty or has no content changes.")
47
+ chosen = typer.prompt("Enter your commit message")
48
+ subprocess.run(["git", "commit", "-m", chosen])
49
+ typer.echo(f"\n Committed: {chosen}")
50
+ raise typer.Exit()
51
+
52
+ config = load_config()
53
+ repo_name = get_repo_name()
54
+ prompt = build_commit_prompt(diff, repo_name, emoji=config["emoji"], commit_style=config["commit_style"])
55
+
56
+ typer.echo("Generating commit message suggestions...")
57
+ suggestions = get_commit_suggestions(prompt)
58
+
59
+ if not suggestions:
60
+ typer.echo("No suggestions generated. Please try again.")
61
+ raise typer.Exit()
62
+
63
+ clean = [s.split(". ", 1)[1] if ". " in s else s for s in suggestions]
64
+ clean.append("Write my own")
65
+
66
+ chosen = questionary.select(
67
+ "Choose a commit message:",
68
+ choices=clean
69
+ ).ask()
70
+
71
+ if chosen is None:
72
+ typer.echo("Aborted.")
73
+ raise typer.Exit()
74
+
75
+ if chosen == "Write my own":
76
+ chosen = typer.prompt("Enter your custom commit message")
77
+
78
+ subprocess.run(["git", "commit", "-m", chosen])
79
+ typer.echo(f"\n Committed: {chosen}")
80
+
81
+
82
+ @app.command()
83
+ def config():
84
+ """View and update gitai settings."""
85
+ current = load_config()
86
+
87
+ typer.echo("Current configuration:\n")
88
+ for key, value in current.items():
89
+ typer.echo(f" {key}: {value}")
90
+
91
+ typer.echo("")
92
+ if not typer.confirm("Do you want to change any settings?"):
93
+ raise typer.Exit()
94
+
95
+ provider = typer.prompt(
96
+ "Provider (ollama, openai, anthropic, gemini)",
97
+ default=current["provider"],
98
+ )
99
+ if provider not in VALID_PROVIDERS:
100
+ typer.echo(f"[gitai] Unknown provider '{provider}'. Choose from: {', '.join(sorted(VALID_PROVIDERS))}")
101
+ raise typer.Exit(code=1)
102
+
103
+ model = typer.prompt("Model name", default=current["model"])
104
+
105
+ ollama_url = current["ollama_url"]
106
+ if provider == "ollama":
107
+ ollama_url = typer.prompt("Ollama URL", default=current["ollama_url"])
108
+
109
+ commit_style = typer.prompt(
110
+ "Commit style (conventional, free-form)",
111
+ default=current["commit_style"],
112
+ )
113
+ if commit_style not in VALID_COMMIT_STYLES:
114
+ typer.echo(f"[gitai] Unknown commit style '{commit_style}'. Choose from: {', '.join(sorted(VALID_COMMIT_STYLES))}")
115
+ raise typer.Exit(code=1)
116
+
117
+ emoji = typer.confirm("Use emojis in commit messages?", default=current["emoji"])
118
+
119
+ new_config = {
120
+ "model": model,
121
+ "provider": provider,
122
+ "ollama_url": ollama_url,
123
+ "commit_style": commit_style,
124
+ "emoji": emoji,
125
+ }
126
+
127
+ save_config(new_config)
128
+ typer.echo("\n✅ Config saved to ~/.gitai.toml")
129
+
130
+
131
+ if __name__ == "__main__":
132
+ app()
gitai/config.py ADDED
@@ -0,0 +1,27 @@
1
+ import tomllib
2
+ import tomli_w
3
+ from pathlib import Path
4
+
5
+ CONFIG_PATH = Path.home() / ".gitai.toml"
6
+
7
+ DEFAULT_CONFIG = {
8
+ "model": "llama3.2",
9
+ "provider": "ollama",
10
+ "ollama_url": "http://localhost:11434",
11
+ "commit_style": "conventional",
12
+ "emoji": False,
13
+ }
14
+
15
+ def load_config() -> dict:
16
+ if not CONFIG_PATH.exists():
17
+ save_config(DEFAULT_CONFIG)
18
+ return DEFAULT_CONFIG.copy()
19
+
20
+ with open(CONFIG_PATH, "rb") as f:
21
+ user_config = tomllib.load(f)
22
+
23
+ return {**DEFAULT_CONFIG, **user_config}
24
+
25
+ def save_config(config: dict) -> None:
26
+ with open(CONFIG_PATH, "wb") as f:
27
+ tomli_w.dump(config, f)
gitai/git.py ADDED
@@ -0,0 +1,24 @@
1
+ import subprocess
2
+ from pathlib import Path
3
+
4
+ def get_staged_diff() -> str:
5
+ result = subprocess.run(
6
+ ["git", "diff", "--cached"],
7
+ capture_output=True,
8
+ text=True,
9
+ encoding="utf-8",
10
+ )
11
+ return result.stdout
12
+
13
+ def get_repo_name() -> str:
14
+ path = Path.cwd()
15
+ return path.name
16
+
17
+ def is_diff_meaningful(diff: str) -> bool:
18
+ meaningful_lines = [
19
+ line for line in diff.splitlines()
20
+ if line.startswith(("+", "-"))
21
+ and not line.startswith(("+++", "---"))
22
+ and line.strip() not in ("+", "-", "")
23
+ ]
24
+ return len(meaningful_lines) > 0
gitai/prompt.py ADDED
@@ -0,0 +1,73 @@
1
+ def build_commit_prompt(diff: str, repo_name: str, emoji: bool = False, commit_style: str = "conventional") -> str:
2
+ format_rules = _build_format_rules(commit_style, emoji)
3
+ example = _build_example(commit_style, emoji)
4
+
5
+ return f"""You are an expert developer generating git commit messages.
6
+
7
+ Repository: {repo_name}
8
+
9
+ Analyze the git diff below and return exactly 3 commit message suggestions.
10
+
11
+ Rules:
12
+ {format_rules}
13
+ - Each suggestion must offer a meaningfully different angle — vary the type, scope, or emphasis
14
+ - Base suggestions strictly on what you see in the diff — never invent context
15
+ - Output: a numbered list of exactly 3 lines, no intro, no explanation, nothing else
16
+
17
+ Example output:
18
+ {example}
19
+
20
+ Git diff:
21
+ {diff}
22
+ """
23
+
24
+
25
+ def _build_format_rules(commit_style: str, emoji: bool) -> str:
26
+ if commit_style == "free-form":
27
+ rules = (
28
+ "- Format: a single clear imperative sentence, no type prefix required\n"
29
+ "- Keep the message under 72 characters\n"
30
+ '- Imperative mood ("add" not "added"), lowercase, no trailing period'
31
+ )
32
+ if emoji:
33
+ rules += "\n- Prefix each message with a relevant emoji that reflects the nature of the change"
34
+ else: # conventional (default)
35
+ rules = (
36
+ "- Format: type(scope): description\n"
37
+ "- Types: feat, fix, refactor, chore, docs, style, test, perf, ci, build\n"
38
+ "- Scope: infer from the file paths or module names changed; omit if unclear\n"
39
+ '- Description: imperative mood ("add" not "added"), lowercase, no trailing period, under 72 characters'
40
+ )
41
+ if emoji:
42
+ rules += (
43
+ "\n- Prefix each message with the matching gitmoji before the type: "
44
+ "✨ feat, 🐛 fix, ♻️ refactor, 🔧 chore, 📝 docs, 🎨 style, ✅ test, ⚡ perf, 👷 ci, 📦 build"
45
+ )
46
+ return rules
47
+
48
+
49
+ def _build_example(commit_style: str, emoji: bool) -> str:
50
+ if commit_style == "free-form":
51
+ if emoji:
52
+ return (
53
+ "1. ✨ add refresh token rotation on session expiry\n"
54
+ "2. 🐛 prevent reuse of invalidated tokens\n"
55
+ "3. ♻️ extract token validation into dedicated service"
56
+ )
57
+ return (
58
+ "1. add refresh token rotation on session expiry\n"
59
+ "2. prevent reuse of invalidated tokens\n"
60
+ "3. extract token validation into dedicated service"
61
+ )
62
+ else: # conventional
63
+ if emoji:
64
+ return (
65
+ "1. ✨ feat(auth): add refresh token rotation on expiry\n"
66
+ "2. 🐛 fix(auth): prevent reuse of invalidated tokens\n"
67
+ "3. ♻️ refactor(auth): extract token validation into dedicated service"
68
+ )
69
+ return (
70
+ "1. feat(auth): add refresh token rotation on expiry\n"
71
+ "2. fix(auth): prevent reuse of invalidated tokens\n"
72
+ "3. refactor(auth): extract token validation into dedicated service"
73
+ )
@@ -0,0 +1,139 @@
1
+ Metadata-Version: 2.4
2
+ Name: gitai-cli
3
+ Version: 0.1.0
4
+ Summary: AI-powered git commit message generator
5
+ Project-URL: Homepage, https://github.com/Jeranguz/gitai
6
+ Project-URL: Repository, https://github.com/Jeranguz/gitai
7
+ Project-URL: Issues, https://github.com/Jeranguz/gitai/issues
8
+ Author: Jeranguz
9
+ License: MIT
10
+ License-File: LICENSE
11
+ Keywords: ai,cli,commit,conventional-commits,git,llm
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Environment :: Console
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Operating System :: OS Independent
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Topic :: Software Development :: Version Control :: Git
21
+ Classifier: Topic :: Utilities
22
+ Requires-Python: >=3.11
23
+ Requires-Dist: gitpython
24
+ Requires-Dist: litellm
25
+ Requires-Dist: questionary
26
+ Requires-Dist: rich
27
+ Requires-Dist: tomli-w
28
+ Requires-Dist: typer
29
+ Provides-Extra: dev
30
+ Requires-Dist: pytest; extra == 'dev'
31
+ Description-Content-Type: text/markdown
32
+
33
+ # gitai
34
+
35
+ <p align="center">
36
+ <img src="assets/commit-genie.png" alt="The Commit Genie" width="200"/>
37
+ </p>
38
+
39
+ <p align="center">
40
+ <a href="https://pypi.org/project/gitai"><img src="https://img.shields.io/pypi/v/gitai" alt="PyPI version"/></a>
41
+ <a href="https://pypi.org/project/gitai"><img src="https://img.shields.io/pypi/pyversions/gitai" alt="Python versions"/></a>
42
+ <a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-blue" alt="MIT license"/></a>
43
+ </p>
44
+
45
+ AI-powered git commit message generator. Analyzes your staged changes and suggests meaningful commit messages — using any LLM you already have access to.
46
+
47
+ ## Features
48
+
49
+ - Reads your staged `git diff` and generates 3 commit message suggestions
50
+ - Interactive selection: pick a suggestion or write your own
51
+ - Supports multiple providers: Ollama (local), OpenAI, Anthropic, Gemini, and [more](https://docs.litellm.ai/docs/providers)
52
+ - Two commit styles: [Conventional Commits](https://www.conventionalcommits.org/) or free-form
53
+ - Optional emoji (gitmoji) support
54
+
55
+ ## Installation
56
+
57
+ ```bash
58
+ pip install gitai
59
+ ```
60
+
61
+ Requires Python 3.11+.
62
+
63
+ ## Quick start
64
+
65
+ ```bash
66
+ # 1. Stage your changes
67
+ git add .
68
+
69
+ # 2. Run gitai
70
+ gitai commit
71
+ ```
72
+
73
+ gitai reads the diff, calls your configured LLM, and presents 3 suggestions to choose from.
74
+
75
+ ## Usage
76
+
77
+ ```
78
+ gitai commit Generate commit message suggestions for staged changes
79
+ gitai config View and update settings
80
+ gitai --version Show version
81
+ gitai --help Show help
82
+ ```
83
+
84
+ ## Configuration
85
+
86
+ Run `gitai config` to update settings interactively. Settings are stored in `~/.gitai.toml`.
87
+
88
+ | Key | Default | Description |
89
+ |---|---|---|
90
+ | `provider` | `ollama` | LLM provider |
91
+ | `model` | `llama3.2` | Model name |
92
+ | `ollama_url` | `http://localhost:11434` | Ollama API base URL (Ollama only) |
93
+ | `commit_style` | `conventional` | `conventional` or `free-form` |
94
+ | `emoji` | `false` | Prefix suggestions with gitmoji |
95
+
96
+ ### Supported providers
97
+
98
+ | Provider | `provider` value | Example `model` value | API key env var |
99
+ |---|---|---|---|
100
+ | Ollama (local) | `ollama` | `llama3.2`, `mistral` | — |
101
+ | Anthropic | `anthropic` | `claude-sonnet-4-6`, `claude-haiku-4-5-20251001` | `ANTHROPIC_API_KEY` |
102
+ | OpenAI | `openai` | `gpt-4o`, `gpt-4o-mini` | `OPENAI_API_KEY` |
103
+ | Gemini | `gemini` | `gemini-2.0-flash` | `GEMINI_API_KEY` |
104
+
105
+ For cloud providers, set the API key in your shell profile:
106
+
107
+ **bash/zsh** (`~/.bashrc` or `~/.zshrc`):
108
+ ```bash
109
+ export ANTHROPIC_API_KEY=sk-ant-...
110
+ ```
111
+
112
+ **PowerShell** (`$PROFILE`):
113
+ ```powershell
114
+ $env:ANTHROPIC_API_KEY="sk-ant-..."
115
+ ```
116
+
117
+ ### Example `~/.gitai.toml`
118
+
119
+ ```toml
120
+ provider = "anthropic"
121
+ model = "claude-haiku-4-5-20251001"
122
+ commit_style = "conventional"
123
+ emoji = false
124
+ ollama_url = "http://localhost:11434"
125
+ ```
126
+
127
+ ## Local setup (Ollama)
128
+
129
+ If you want to run fully offline with Ollama:
130
+
131
+ 1. Install [Ollama](https://ollama.com/)
132
+ 2. Pull a model: `ollama pull llama3.2`
133
+ 3. Run `gitai commit` — no API key needed
134
+
135
+ ## TODO
136
+
137
+ - [ ] Allow configuring the number of suggestions generated
138
+ - [ ] Add `--push` flag to commit and push in one step
139
+ - [ ] Support unstaged changes with an optional `--all` flag
@@ -0,0 +1,11 @@
1
+ gitai/__init__.py,sha256=kUR5RAFc7HCeiqdlX36dZOHkUI5wI6V_43RpEcD8b-0,22
2
+ gitai/ai.py,sha256=wx_yYA865VsQB8SsrzkEKd1hLF7SmdsWf1j-cbWeemU,1656
3
+ gitai/cli.py,sha256=pyiJS7jm9IRgeYdsJX4-v77zramPo_BXF9hghchmLi4,3949
4
+ gitai/config.py,sha256=LqmaFN9JBpHL0MGbn476sHBGUgfhfmEeQYD-7J60yxE,669
5
+ gitai/git.py,sha256=iDv09764q21uRBVJk784-qo9Gl3Nrpk0rpW31ha9_p0,628
6
+ gitai/prompt.py,sha256=Hj61wFDOrDztmnQxLNWagjCJ6OXfqWreS93z0KfTE_o,3057
7
+ gitai_cli-0.1.0.dist-info/METADATA,sha256=HS5B9TjSQ8OTbx_Yfbn6BXM16ew3H7KQPadtmtJB-Go,4257
8
+ gitai_cli-0.1.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
9
+ gitai_cli-0.1.0.dist-info/entry_points.txt,sha256=Cv684a3aG7n1URVEr6skYx27B37CJoev2fZx2irdUiw,40
10
+ gitai_cli-0.1.0.dist-info/licenses/LICENSE,sha256=RjOVI-Vu4fAe5qNurmnVBMVROuK_xPePzpCSEEEtKq4,1065
11
+ gitai_cli-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.29.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ gitai = gitai.cli:app
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Jeranguz
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.