fm-smart-commit 1.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.
- fm_smart_commit-1.1.0/LICENSE +21 -0
- fm_smart_commit-1.1.0/PKG-INFO +98 -0
- fm_smart_commit-1.1.0/README.md +75 -0
- fm_smart_commit-1.1.0/pyproject.toml +49 -0
- fm_smart_commit-1.1.0/setup.cfg +4 -0
- fm_smart_commit-1.1.0/src/fm_smart_commit.egg-info/PKG-INFO +98 -0
- fm_smart_commit-1.1.0/src/fm_smart_commit.egg-info/SOURCES.txt +11 -0
- fm_smart_commit-1.1.0/src/fm_smart_commit.egg-info/dependency_links.txt +1 -0
- fm_smart_commit-1.1.0/src/fm_smart_commit.egg-info/entry_points.txt +2 -0
- fm_smart_commit-1.1.0/src/fm_smart_commit.egg-info/requires.txt +5 -0
- fm_smart_commit-1.1.0/src/fm_smart_commit.egg-info/top_level.txt +1 -0
- fm_smart_commit-1.1.0/src/smartcommit/__init__.py +211 -0
- fm_smart_commit-1.1.0/tests/test_smartcommit.py +233 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Maverick B.
|
|
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,98 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: fm-smart-commit
|
|
3
|
+
Version: 1.1.0
|
|
4
|
+
Summary: AI-powered Git commit message generator using Apple Intelligence
|
|
5
|
+
Author-email: Maverick Brazill <brazillmav@gmail.com>
|
|
6
|
+
Classifier: Development Status :: 3 - Alpha
|
|
7
|
+
Classifier: Environment :: Console
|
|
8
|
+
Classifier: Intended Audience :: Developers
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
15
|
+
Requires-Python: >=3.10
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
License-File: LICENSE
|
|
18
|
+
Requires-Dist: apple_fm_sdk
|
|
19
|
+
Provides-Extra: dev
|
|
20
|
+
Requires-Dist: pytest; extra == "dev"
|
|
21
|
+
Requires-Dist: pytest-asyncio; extra == "dev"
|
|
22
|
+
Dynamic: license-file
|
|
23
|
+
|
|
24
|
+
# Git SmartCommit CLI
|
|
25
|
+
|
|
26
|
+
**AI-powered Git commit generation running *100% locally* on your Mac using Apple Intelligence.**
|
|
27
|
+
|
|
28
|
+
SmartCommit is a lightweight CLI tool that analyzes your staged Git changes and generates concise, professional [Conventional Commits](https://www.conventionalcommits.org/) (`feat:`, `fix:`, `refactor:`, etc.).
|
|
29
|
+
|
|
30
|
+
Because it runs completely on-device using the Apple Foundation Models SDK, it offers massive advantages over cloud-based AI tools:
|
|
31
|
+
|
|
32
|
+
* **Privacy-First:** Your codebase never leaves your machine. Perfect for enterprise, proprietary, or sensitive code.
|
|
33
|
+
* **Zero API Costs:** No OpenAI API keys or GitHub Copilot subscriptions required. It uses the AI already built into your Mac.
|
|
34
|
+
* ️**Fast:** Powered natively by Apple Silicon neural engines for instant inference.
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## Prerequisites
|
|
39
|
+
|
|
40
|
+
Before installing, ensure your machine meets the hardware and software requirements for local Apple Intelligence:
|
|
41
|
+
* **Hardware:** Apple Silicon Mac (M1 chip or newer).
|
|
42
|
+
* **OS:** macOS 15.0 (Sequoia) or newer.
|
|
43
|
+
* **Settings:** Apple Intelligence must be enabled on your Mac.
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## Installation
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
# 1. Download the latest release
|
|
51
|
+
curl -fsSL -o smartcommit https://github.com/brazill7/smart-commit/releases/latest/download/smartcommit
|
|
52
|
+
|
|
53
|
+
# 2. Make the file executable
|
|
54
|
+
chmod +x smartcommit
|
|
55
|
+
|
|
56
|
+
# 3. Clear the macOS Gatekeeper quarantine flag (required for unsigned binaries)
|
|
57
|
+
xattr -d com.apple.quarantine smartcommit
|
|
58
|
+
|
|
59
|
+
# 4. Move it to your local bin so it can be run from anywhere
|
|
60
|
+
sudo mv smartcommit /usr/local/bin/
|
|
61
|
+
|
|
62
|
+
### (Optional) Set up a Git Alias
|
|
63
|
+
If you want to use this tool natively within Git (e.g., typing `git sc` or `git smart-commit` etc. ), you can add a global alias:
|
|
64
|
+
|
|
65
|
+
git config --global alias.sc '!smartcommit'
|
|
66
|
+
git config --global alias.smart-commit '!smartcommit'
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
## Usage
|
|
72
|
+
|
|
73
|
+
Make sure you have staged your changes (`git add .`) before running the tool.
|
|
74
|
+
|
|
75
|
+
### Option A: Using the Standalone Command
|
|
76
|
+
If you skipped the alias step, you can just call the tool directly in your repository:
|
|
77
|
+
|
|
78
|
+
`smartcommit`
|
|
79
|
+
|
|
80
|
+
To provide custom context to the AI (like explaining *why* you made a change), use the `-c` flag:
|
|
81
|
+
|
|
82
|
+
`smartcommit -c "race condition on the login screen"`
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
### Option B: Using the Git Alias
|
|
86
|
+
If you configured the `git smart-commit` alias, you can use it just like a native Git command:
|
|
87
|
+
|
|
88
|
+
`git smart-commit`
|
|
89
|
+
|
|
90
|
+
With custom context:
|
|
91
|
+
|
|
92
|
+
`git smart-commit -c "refactored the login auth flow"`
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
**Example Output:**
|
|
96
|
+
> Analyzing diff...
|
|
97
|
+
> Suggested commit: **fix: resolve race condition in login flow**
|
|
98
|
+
> Accept this commit message? (y/n):
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# Git SmartCommit CLI
|
|
2
|
+
|
|
3
|
+
**AI-powered Git commit generation running *100% locally* on your Mac using Apple Intelligence.**
|
|
4
|
+
|
|
5
|
+
SmartCommit is a lightweight CLI tool that analyzes your staged Git changes and generates concise, professional [Conventional Commits](https://www.conventionalcommits.org/) (`feat:`, `fix:`, `refactor:`, etc.).
|
|
6
|
+
|
|
7
|
+
Because it runs completely on-device using the Apple Foundation Models SDK, it offers massive advantages over cloud-based AI tools:
|
|
8
|
+
|
|
9
|
+
* **Privacy-First:** Your codebase never leaves your machine. Perfect for enterprise, proprietary, or sensitive code.
|
|
10
|
+
* **Zero API Costs:** No OpenAI API keys or GitHub Copilot subscriptions required. It uses the AI already built into your Mac.
|
|
11
|
+
* ️**Fast:** Powered natively by Apple Silicon neural engines for instant inference.
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## Prerequisites
|
|
16
|
+
|
|
17
|
+
Before installing, ensure your machine meets the hardware and software requirements for local Apple Intelligence:
|
|
18
|
+
* **Hardware:** Apple Silicon Mac (M1 chip or newer).
|
|
19
|
+
* **OS:** macOS 15.0 (Sequoia) or newer.
|
|
20
|
+
* **Settings:** Apple Intelligence must be enabled on your Mac.
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## Installation
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
# 1. Download the latest release
|
|
28
|
+
curl -fsSL -o smartcommit https://github.com/brazill7/smart-commit/releases/latest/download/smartcommit
|
|
29
|
+
|
|
30
|
+
# 2. Make the file executable
|
|
31
|
+
chmod +x smartcommit
|
|
32
|
+
|
|
33
|
+
# 3. Clear the macOS Gatekeeper quarantine flag (required for unsigned binaries)
|
|
34
|
+
xattr -d com.apple.quarantine smartcommit
|
|
35
|
+
|
|
36
|
+
# 4. Move it to your local bin so it can be run from anywhere
|
|
37
|
+
sudo mv smartcommit /usr/local/bin/
|
|
38
|
+
|
|
39
|
+
### (Optional) Set up a Git Alias
|
|
40
|
+
If you want to use this tool natively within Git (e.g., typing `git sc` or `git smart-commit` etc. ), you can add a global alias:
|
|
41
|
+
|
|
42
|
+
git config --global alias.sc '!smartcommit'
|
|
43
|
+
git config --global alias.smart-commit '!smartcommit'
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## Usage
|
|
49
|
+
|
|
50
|
+
Make sure you have staged your changes (`git add .`) before running the tool.
|
|
51
|
+
|
|
52
|
+
### Option A: Using the Standalone Command
|
|
53
|
+
If you skipped the alias step, you can just call the tool directly in your repository:
|
|
54
|
+
|
|
55
|
+
`smartcommit`
|
|
56
|
+
|
|
57
|
+
To provide custom context to the AI (like explaining *why* you made a change), use the `-c` flag:
|
|
58
|
+
|
|
59
|
+
`smartcommit -c "race condition on the login screen"`
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
### Option B: Using the Git Alias
|
|
63
|
+
If you configured the `git smart-commit` alias, you can use it just like a native Git command:
|
|
64
|
+
|
|
65
|
+
`git smart-commit`
|
|
66
|
+
|
|
67
|
+
With custom context:
|
|
68
|
+
|
|
69
|
+
`git smart-commit -c "refactored the login auth flow"`
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
**Example Output:**
|
|
73
|
+
> Analyzing diff...
|
|
74
|
+
> Suggested commit: **fix: resolve race condition in login flow**
|
|
75
|
+
> Accept this commit message? (y/n):
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "fm-smart-commit"
|
|
7
|
+
version = "1.1.0"
|
|
8
|
+
description = "AI-powered Git commit message generator using Apple Intelligence"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
authors = [
|
|
12
|
+
{name = "Maverick Brazill", email = "brazillmav@gmail.com"}
|
|
13
|
+
]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 3 - Alpha",
|
|
16
|
+
"Environment :: Console",
|
|
17
|
+
"Intended Audience :: Developers",
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"Programming Language :: Python :: 3.10",
|
|
20
|
+
"Programming Language :: Python :: 3.11",
|
|
21
|
+
"Programming Language :: Python :: 3.12",
|
|
22
|
+
"Programming Language :: Python :: 3.13",
|
|
23
|
+
"Programming Language :: Python :: 3.14",
|
|
24
|
+
]
|
|
25
|
+
dependencies = [
|
|
26
|
+
"apple_fm_sdk",
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
[project.scripts]
|
|
30
|
+
smartcommit = "smartcommit:main"
|
|
31
|
+
|
|
32
|
+
[project.optional-dependencies]
|
|
33
|
+
dev = [
|
|
34
|
+
"pytest",
|
|
35
|
+
"pytest-asyncio",
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
[tool.setuptools.packages.find]
|
|
39
|
+
where = ["src"]
|
|
40
|
+
include = ["smartcommit*"]
|
|
41
|
+
|
|
42
|
+
[tool.pypi]
|
|
43
|
+
twine = "dist"
|
|
44
|
+
|
|
45
|
+
[tool.uv]
|
|
46
|
+
dev-dependencies = [
|
|
47
|
+
"pytest",
|
|
48
|
+
"pytest-asyncio",
|
|
49
|
+
]
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: fm-smart-commit
|
|
3
|
+
Version: 1.1.0
|
|
4
|
+
Summary: AI-powered Git commit message generator using Apple Intelligence
|
|
5
|
+
Author-email: Maverick Brazill <brazillmav@gmail.com>
|
|
6
|
+
Classifier: Development Status :: 3 - Alpha
|
|
7
|
+
Classifier: Environment :: Console
|
|
8
|
+
Classifier: Intended Audience :: Developers
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
15
|
+
Requires-Python: >=3.10
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
License-File: LICENSE
|
|
18
|
+
Requires-Dist: apple_fm_sdk
|
|
19
|
+
Provides-Extra: dev
|
|
20
|
+
Requires-Dist: pytest; extra == "dev"
|
|
21
|
+
Requires-Dist: pytest-asyncio; extra == "dev"
|
|
22
|
+
Dynamic: license-file
|
|
23
|
+
|
|
24
|
+
# Git SmartCommit CLI
|
|
25
|
+
|
|
26
|
+
**AI-powered Git commit generation running *100% locally* on your Mac using Apple Intelligence.**
|
|
27
|
+
|
|
28
|
+
SmartCommit is a lightweight CLI tool that analyzes your staged Git changes and generates concise, professional [Conventional Commits](https://www.conventionalcommits.org/) (`feat:`, `fix:`, `refactor:`, etc.).
|
|
29
|
+
|
|
30
|
+
Because it runs completely on-device using the Apple Foundation Models SDK, it offers massive advantages over cloud-based AI tools:
|
|
31
|
+
|
|
32
|
+
* **Privacy-First:** Your codebase never leaves your machine. Perfect for enterprise, proprietary, or sensitive code.
|
|
33
|
+
* **Zero API Costs:** No OpenAI API keys or GitHub Copilot subscriptions required. It uses the AI already built into your Mac.
|
|
34
|
+
* ️**Fast:** Powered natively by Apple Silicon neural engines for instant inference.
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## Prerequisites
|
|
39
|
+
|
|
40
|
+
Before installing, ensure your machine meets the hardware and software requirements for local Apple Intelligence:
|
|
41
|
+
* **Hardware:** Apple Silicon Mac (M1 chip or newer).
|
|
42
|
+
* **OS:** macOS 15.0 (Sequoia) or newer.
|
|
43
|
+
* **Settings:** Apple Intelligence must be enabled on your Mac.
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## Installation
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
# 1. Download the latest release
|
|
51
|
+
curl -fsSL -o smartcommit https://github.com/brazill7/smart-commit/releases/latest/download/smartcommit
|
|
52
|
+
|
|
53
|
+
# 2. Make the file executable
|
|
54
|
+
chmod +x smartcommit
|
|
55
|
+
|
|
56
|
+
# 3. Clear the macOS Gatekeeper quarantine flag (required for unsigned binaries)
|
|
57
|
+
xattr -d com.apple.quarantine smartcommit
|
|
58
|
+
|
|
59
|
+
# 4. Move it to your local bin so it can be run from anywhere
|
|
60
|
+
sudo mv smartcommit /usr/local/bin/
|
|
61
|
+
|
|
62
|
+
### (Optional) Set up a Git Alias
|
|
63
|
+
If you want to use this tool natively within Git (e.g., typing `git sc` or `git smart-commit` etc. ), you can add a global alias:
|
|
64
|
+
|
|
65
|
+
git config --global alias.sc '!smartcommit'
|
|
66
|
+
git config --global alias.smart-commit '!smartcommit'
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
## Usage
|
|
72
|
+
|
|
73
|
+
Make sure you have staged your changes (`git add .`) before running the tool.
|
|
74
|
+
|
|
75
|
+
### Option A: Using the Standalone Command
|
|
76
|
+
If you skipped the alias step, you can just call the tool directly in your repository:
|
|
77
|
+
|
|
78
|
+
`smartcommit`
|
|
79
|
+
|
|
80
|
+
To provide custom context to the AI (like explaining *why* you made a change), use the `-c` flag:
|
|
81
|
+
|
|
82
|
+
`smartcommit -c "race condition on the login screen"`
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
### Option B: Using the Git Alias
|
|
86
|
+
If you configured the `git smart-commit` alias, you can use it just like a native Git command:
|
|
87
|
+
|
|
88
|
+
`git smart-commit`
|
|
89
|
+
|
|
90
|
+
With custom context:
|
|
91
|
+
|
|
92
|
+
`git smart-commit -c "refactored the login auth flow"`
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
**Example Output:**
|
|
96
|
+
> Analyzing diff...
|
|
97
|
+
> Suggested commit: **fix: resolve race condition in login flow**
|
|
98
|
+
> Accept this commit message? (y/n):
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
src/fm_smart_commit.egg-info/PKG-INFO
|
|
5
|
+
src/fm_smart_commit.egg-info/SOURCES.txt
|
|
6
|
+
src/fm_smart_commit.egg-info/dependency_links.txt
|
|
7
|
+
src/fm_smart_commit.egg-info/entry_points.txt
|
|
8
|
+
src/fm_smart_commit.egg-info/requires.txt
|
|
9
|
+
src/fm_smart_commit.egg-info/top_level.txt
|
|
10
|
+
src/smartcommit/__init__.py
|
|
11
|
+
tests/test_smartcommit.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
smartcommit
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import subprocess
|
|
2
|
+
import asyncio
|
|
3
|
+
import argparse
|
|
4
|
+
from typing import Optional
|
|
5
|
+
import apple_fm_sdk as fm
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
async def analyze_scope(diff: str, session: fm.LanguageModelSession) -> str:
|
|
9
|
+
prompt = f"""
|
|
10
|
+
Analyze this git diff and identify the SCOPE of changes: what files, components, or modules are affected?
|
|
11
|
+
|
|
12
|
+
Diff:
|
|
13
|
+
{diff}
|
|
14
|
+
|
|
15
|
+
Output ONLY a brief list of affected areas (e.g., "user_auth.py, login component, API routes"). No other text.
|
|
16
|
+
"""
|
|
17
|
+
response = await session.respond(prompt)
|
|
18
|
+
return response.strip()
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
async def analyze_intent(diff: str, session: fm.LanguageModelSession, developer_context: Optional[str] = None) -> str:
|
|
22
|
+
context = f"\nDeveloper context: {developer_context}" if developer_context else ""
|
|
23
|
+
prompt = f"""
|
|
24
|
+
Analyze this git diff and identify the INTENT: why was this change made? What problem does it solve?{context}
|
|
25
|
+
|
|
26
|
+
Diff:
|
|
27
|
+
{diff}
|
|
28
|
+
|
|
29
|
+
Output ONLY a one-sentence intent summary. No other text.
|
|
30
|
+
"""
|
|
31
|
+
response = await session.respond(prompt)
|
|
32
|
+
return response.strip()
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
async def analyze_changes(diff: str, session: fm.LanguageModelSession) -> str:
|
|
36
|
+
prompt = f"""
|
|
37
|
+
Analyze this git diff and identify the SPECIFIC CHANGES: what functions, logic, or code patterns changed?
|
|
38
|
+
|
|
39
|
+
Diff:
|
|
40
|
+
{diff}
|
|
41
|
+
|
|
42
|
+
Output ONLY a brief list of what changed (e.g., "added calculateTotal function, refactored validation logic"). No other text.
|
|
43
|
+
"""
|
|
44
|
+
response = await session.respond(prompt)
|
|
45
|
+
return response.strip()
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
async def synthesize_message(scope: str, intent: str, changes: str, session: fm.LanguageModelSession) -> str:
|
|
49
|
+
prompt = f"""
|
|
50
|
+
You are a Git commit message generator. Using the following analysis, generate a SINGLE Conventional Commit message.
|
|
51
|
+
|
|
52
|
+
Scope (what files/components): {scope}
|
|
53
|
+
Intent (why): {intent}
|
|
54
|
+
Changes (what): {changes}
|
|
55
|
+
|
|
56
|
+
Rules:
|
|
57
|
+
- Use prefix: feat:, fix:, docs:, style:, refactor:, chore:
|
|
58
|
+
- Keep it under 72 characters if possible
|
|
59
|
+
- ONLY output the commit message, no quotes, no explanation
|
|
60
|
+
|
|
61
|
+
Output:
|
|
62
|
+
"""
|
|
63
|
+
response = await session.respond(prompt)
|
|
64
|
+
return response.strip().strip('"').strip("'")
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
async def quick_mode(developer_context: Optional[str] = None) -> Optional[str]:
|
|
68
|
+
diff_process = subprocess.run(['git', 'diff', '--staged'], capture_output=True, text=True)
|
|
69
|
+
diff = diff_process.stdout.strip()
|
|
70
|
+
|
|
71
|
+
if not diff:
|
|
72
|
+
print("No staged changes found. Run `git add` first!")
|
|
73
|
+
return None
|
|
74
|
+
|
|
75
|
+
model = fm.SystemLanguageModel()
|
|
76
|
+
is_available, reason = model.is_available()
|
|
77
|
+
if not is_available:
|
|
78
|
+
print(f"Apple Intelligence unavailable: {reason}")
|
|
79
|
+
return None
|
|
80
|
+
|
|
81
|
+
session = fm.LanguageModelSession()
|
|
82
|
+
|
|
83
|
+
context_instruction = ""
|
|
84
|
+
if developer_context:
|
|
85
|
+
context_instruction = f"\nAdditional context from the developer to include/consider:\n\"{developer_context}\"\n"
|
|
86
|
+
|
|
87
|
+
prompt = f"""
|
|
88
|
+
You are a strictly formatted Git commit generator. Analyze the following code diff and generate a single commit message using the Conventional Commits standard.
|
|
89
|
+
|
|
90
|
+
Allowed prefixes: feat:, fix:, docs:, style:, refactor:, chore:
|
|
91
|
+
|
|
92
|
+
Rules:
|
|
93
|
+
- ONLY output the commit message. No conversational text.
|
|
94
|
+
- Do not wrap the output in quotes.
|
|
95
|
+
- Incorporate any additional context provided by the developer.
|
|
96
|
+
|
|
97
|
+
Examples:
|
|
98
|
+
Diff: + function calculateTotal(a, b) {{ return a + b; }}
|
|
99
|
+
Output: feat: add calculateTotal function, which adds a and b and returns the total.
|
|
100
|
+
|
|
101
|
+
Actual Diff to analyze:
|
|
102
|
+
{diff}
|
|
103
|
+
{context_instruction}
|
|
104
|
+
|
|
105
|
+
Analyze the following code diff and generate a single commit message using the Conventional Commits standard.
|
|
106
|
+
|
|
107
|
+
Allowed prefixes: feat:, fix:, docs:, style:, refactor:, chore:
|
|
108
|
+
|
|
109
|
+
Output:
|
|
110
|
+
"""
|
|
111
|
+
|
|
112
|
+
print("Analyzing diff (quick mode)...")
|
|
113
|
+
response = await session.respond(prompt)
|
|
114
|
+
return response.strip().strip('"').strip("'")
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
async def detailed_mode(developer_context: Optional[str] = None) -> Optional[str]:
|
|
118
|
+
diff_process = subprocess.run(['git', 'diff', '--staged'], capture_output=True, text=True)
|
|
119
|
+
diff = diff_process.stdout.strip()
|
|
120
|
+
|
|
121
|
+
if not diff:
|
|
122
|
+
print("No staged changes found. Run `git add` first!")
|
|
123
|
+
return None
|
|
124
|
+
|
|
125
|
+
model = fm.SystemLanguageModel()
|
|
126
|
+
is_available, reason = model.is_available()
|
|
127
|
+
if not is_available:
|
|
128
|
+
print(f"Apple Intelligence unavailable: {reason}")
|
|
129
|
+
return None
|
|
130
|
+
|
|
131
|
+
session = fm.LanguageModelSession()
|
|
132
|
+
|
|
133
|
+
print("Analyzing diff (detailed mode - 3 parallel agents)...")
|
|
134
|
+
|
|
135
|
+
commit_msg = None
|
|
136
|
+
try:
|
|
137
|
+
async with asyncio.TaskGroup() as tg:
|
|
138
|
+
task_scope = tg.create_task(analyze_scope(diff, session))
|
|
139
|
+
task_intent = tg.create_task(analyze_intent(diff, session, developer_context))
|
|
140
|
+
task_changes = tg.create_task(analyze_changes(diff, session))
|
|
141
|
+
|
|
142
|
+
scope_result = task_scope.result()
|
|
143
|
+
intent_result = task_intent.result()
|
|
144
|
+
changes_result = task_changes.result()
|
|
145
|
+
|
|
146
|
+
print(f" Scope: {scope_result[:50]}...")
|
|
147
|
+
print(f" Intent: {intent_result[:50]}...")
|
|
148
|
+
print(f" Changes: {changes_result[:50]}...")
|
|
149
|
+
|
|
150
|
+
print("Synthesizing results...")
|
|
151
|
+
commit_msg = await synthesize_message(scope_result, intent_result, changes_result, session)
|
|
152
|
+
|
|
153
|
+
except* Exception as e:
|
|
154
|
+
print(f"Error in parallel analysis: {e}")
|
|
155
|
+
|
|
156
|
+
return commit_msg
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
async def commit_flow(developer_context: Optional[str] = None, quick: bool = False) -> None:
|
|
160
|
+
commit_msg = None
|
|
161
|
+
max_retries = 3
|
|
162
|
+
|
|
163
|
+
for attempt in range(max_retries):
|
|
164
|
+
if quick:
|
|
165
|
+
commit_msg = await quick_mode(developer_context)
|
|
166
|
+
else:
|
|
167
|
+
commit_msg = await detailed_mode(developer_context)
|
|
168
|
+
|
|
169
|
+
if not commit_msg:
|
|
170
|
+
return
|
|
171
|
+
|
|
172
|
+
print(f"\nSuggested commit: \033[92m{commit_msg}\033[0m")
|
|
173
|
+
user_input = input("Accept commit? (y/n/r): ")
|
|
174
|
+
|
|
175
|
+
if user_input.lower() == 'y':
|
|
176
|
+
subprocess.run(['git', 'commit', '-m', commit_msg])
|
|
177
|
+
print("Committed successfully!")
|
|
178
|
+
return
|
|
179
|
+
elif user_input.lower() == 'r':
|
|
180
|
+
print("Retrying...\n")
|
|
181
|
+
continue
|
|
182
|
+
else:
|
|
183
|
+
print("Commit aborted.")
|
|
184
|
+
return
|
|
185
|
+
|
|
186
|
+
print("Max retries reached. Commit aborted.")
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def main():
|
|
190
|
+
parser = argparse.ArgumentParser(description="Generate smart Git commits using Apple Intelligence.")
|
|
191
|
+
parser.add_argument(
|
|
192
|
+
'-c', '--context',
|
|
193
|
+
type=str,
|
|
194
|
+
help='Additional context or intent to guide the AI (e.g., "fixes ticket #123")'
|
|
195
|
+
)
|
|
196
|
+
parser.add_argument(
|
|
197
|
+
'-q', '--quick',
|
|
198
|
+
action='store_true',
|
|
199
|
+
help='Use quick single-pass mode (faster but less detailed)'
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
args = parser.parse_args()
|
|
203
|
+
|
|
204
|
+
if args.quick:
|
|
205
|
+
print("Running in --quick mode\n")
|
|
206
|
+
|
|
207
|
+
asyncio.run(commit_flow(developer_context=args.context, quick=args.quick))
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
if __name__ == "__main__":
|
|
211
|
+
main()
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import unittest
|
|
2
|
+
import asyncio
|
|
3
|
+
from unittest.mock import patch, MagicMock, AsyncMock
|
|
4
|
+
import subprocess
|
|
5
|
+
import sys
|
|
6
|
+
from io import StringIO
|
|
7
|
+
import importlib.util
|
|
8
|
+
|
|
9
|
+
sys.path.insert(0, '..')
|
|
10
|
+
|
|
11
|
+
spec = importlib.util.find_spec('apple_fm_sdk')
|
|
12
|
+
if spec is None:
|
|
13
|
+
sys.modules['apple_fm_sdk'] = MagicMock()
|
|
14
|
+
|
|
15
|
+
from smartcommit import (
|
|
16
|
+
quick_mode,
|
|
17
|
+
detailed_mode,
|
|
18
|
+
analyze_scope,
|
|
19
|
+
analyze_intent,
|
|
20
|
+
analyze_changes,
|
|
21
|
+
synthesize_message,
|
|
22
|
+
commit_flow,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class MockLanguageModelSession:
|
|
27
|
+
def __init__(self):
|
|
28
|
+
pass
|
|
29
|
+
|
|
30
|
+
async def respond(self, prompt: str) -> str:
|
|
31
|
+
if "SCOPE" in prompt:
|
|
32
|
+
return "user_auth.py, login.py, auth module"
|
|
33
|
+
elif "INTENT" in prompt:
|
|
34
|
+
return "To improve user authentication security"
|
|
35
|
+
elif "SPECIFIC CHANGES" in prompt:
|
|
36
|
+
return "added hashPassword function, updated validation"
|
|
37
|
+
elif "synthesize" in prompt.lower() or "generate" in prompt.lower():
|
|
38
|
+
return "feat: add hashPassword function for secure auth"
|
|
39
|
+
else:
|
|
40
|
+
return "feat: update authentication module"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class MockSystemLanguageModel:
|
|
44
|
+
def is_available(self):
|
|
45
|
+
return True, ""
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class TestQuickMode(unittest.IsolatedAsyncioTestCase):
|
|
49
|
+
@patch('smartcommit.subprocess.run')
|
|
50
|
+
@patch('smartcommit.fm.SystemLanguageModel')
|
|
51
|
+
@patch('smartcommit.fm.LanguageModelSession')
|
|
52
|
+
def test_quick_mode_no_staged_changes(self, mock_session, mock_model, mock_run):
|
|
53
|
+
mock_run.return_value = MagicMock(stdout="")
|
|
54
|
+
|
|
55
|
+
result = asyncio.run(quick_mode())
|
|
56
|
+
|
|
57
|
+
self.assertIsNone(result)
|
|
58
|
+
|
|
59
|
+
@patch('smartcommit.subprocess.run')
|
|
60
|
+
@patch('smartcommit.fm.SystemLanguageModel')
|
|
61
|
+
@patch('smartcommit.fm.LanguageModelSession')
|
|
62
|
+
def test_quick_mode_success(self, mock_session, mock_model, mock_run):
|
|
63
|
+
mock_run.return_value = MagicMock(stdout="+ def new_func(): pass")
|
|
64
|
+
mock_model.return_value = MockSystemLanguageModel()
|
|
65
|
+
mock_session.return_value = MockLanguageModelSession()
|
|
66
|
+
|
|
67
|
+
result = asyncio.run(quick_mode())
|
|
68
|
+
|
|
69
|
+
self.assertIsNotNone(result)
|
|
70
|
+
self.assertIn("feat:", result)
|
|
71
|
+
|
|
72
|
+
@patch('smartcommit.subprocess.run')
|
|
73
|
+
@patch('smartcommit.fm.SystemLanguageModel')
|
|
74
|
+
@patch('smartcommit.fm.LanguageModelSession')
|
|
75
|
+
def test_quick_mode_with_context(self, mock_session, mock_model, mock_run):
|
|
76
|
+
mock_run.return_value = MagicMock(stdout="+ def new_func(): pass")
|
|
77
|
+
mock_model.return_value = MockSystemLanguageModel()
|
|
78
|
+
mock_session.return_value = MockLanguageModelSession()
|
|
79
|
+
|
|
80
|
+
result = asyncio.run(quick_mode(developer_context="fixes ticket #123"))
|
|
81
|
+
|
|
82
|
+
self.assertIsNotNone(result)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class TestDetailedMode(unittest.IsolatedAsyncioTestCase):
|
|
86
|
+
@patch('smartcommit.subprocess.run')
|
|
87
|
+
@patch('smartcommit.fm.SystemLanguageModel')
|
|
88
|
+
@patch('smartcommit.fm.LanguageModelSession')
|
|
89
|
+
def test_detailed_mode_no_staged(self, mock_session, mock_model, mock_run):
|
|
90
|
+
mock_run.return_value = MagicMock(stdout="")
|
|
91
|
+
|
|
92
|
+
result = asyncio.run(detailed_mode())
|
|
93
|
+
|
|
94
|
+
self.assertIsNone(result)
|
|
95
|
+
|
|
96
|
+
@patch('smartcommit.subprocess.run')
|
|
97
|
+
@patch('smartcommit.fm.SystemLanguageModel')
|
|
98
|
+
@patch('smartcommit.fm.LanguageModelSession')
|
|
99
|
+
def test_detailed_mode_success(self, mock_session, mock_model, mock_run):
|
|
100
|
+
mock_run.return_value = MagicMock(stdout="+ def new_func(): pass")
|
|
101
|
+
mock_model.return_value = MockSystemLanguageModel()
|
|
102
|
+
mock_session.return_value = MockLanguageModelSession()
|
|
103
|
+
|
|
104
|
+
result = asyncio.run(detailed_mode())
|
|
105
|
+
|
|
106
|
+
self.assertIsNotNone(result)
|
|
107
|
+
self.assertIn("feat:", result)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
class TestAnalyzeFunctions(unittest.IsolatedAsyncioTestCase):
|
|
111
|
+
@patch('smartcommit.fm.LanguageModelSession')
|
|
112
|
+
def test_analyze_scope(self, mock_session):
|
|
113
|
+
mock_session.return_value = MockLanguageModelSession()
|
|
114
|
+
|
|
115
|
+
result = asyncio.run(analyze_scope("+ new file", mock_session.return_value))
|
|
116
|
+
|
|
117
|
+
self.assertIsNotNone(result)
|
|
118
|
+
self.assertIn("user_auth.py", result)
|
|
119
|
+
|
|
120
|
+
@patch('smartcommit.fm.LanguageModelSession')
|
|
121
|
+
def test_analyze_intent(self, mock_session):
|
|
122
|
+
mock_session.return_value = MockLanguageModelSession()
|
|
123
|
+
|
|
124
|
+
result = asyncio.run(analyze_intent("+ new file", mock_session.return_value))
|
|
125
|
+
|
|
126
|
+
self.assertIsNotNone(result)
|
|
127
|
+
|
|
128
|
+
@patch('smartcommit.fm.LanguageModelSession')
|
|
129
|
+
def test_analyze_intent_with_context(self, mock_session):
|
|
130
|
+
mock_session.return_value = MockLanguageModelSession()
|
|
131
|
+
|
|
132
|
+
result = asyncio.run(
|
|
133
|
+
analyze_intent("+ new file", mock_session.return_value, "fixes #456")
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
self.assertIsNotNone(result)
|
|
137
|
+
|
|
138
|
+
@patch('smartcommit.fm.LanguageModelSession')
|
|
139
|
+
def test_analyze_changes(self, mock_session):
|
|
140
|
+
mock_session.return_value = MockLanguageModelSession()
|
|
141
|
+
|
|
142
|
+
result = asyncio.run(analyze_changes("+ new file", mock_session.return_value))
|
|
143
|
+
|
|
144
|
+
self.assertIsNotNone(result)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
class TestSynthesizeMessage(unittest.IsolatedAsyncioTestCase):
|
|
148
|
+
@patch('smartcommit.fm.LanguageModelSession')
|
|
149
|
+
def test_synthesize_message(self, mock_session):
|
|
150
|
+
mock_session.return_value = MockLanguageModelSession()
|
|
151
|
+
|
|
152
|
+
result = asyncio.run(synthesize_message(
|
|
153
|
+
"auth.py",
|
|
154
|
+
"improve security",
|
|
155
|
+
"added hash function",
|
|
156
|
+
mock_session.return_value
|
|
157
|
+
))
|
|
158
|
+
|
|
159
|
+
self.assertIsNotNone(result)
|
|
160
|
+
self.assertIn("feat:", result)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
class TestCommitFlow(unittest.IsolatedAsyncioTestCase):
|
|
164
|
+
@patch('smartcommit.subprocess.run')
|
|
165
|
+
@patch('smartcommit.input', return_value='y')
|
|
166
|
+
@patch('smartcommit.fm.SystemLanguageModel')
|
|
167
|
+
@patch('smartcommit.fm.LanguageModelSession')
|
|
168
|
+
def test_commit_flow_accept(self, mock_session, mock_model, mock_input, mock_run):
|
|
169
|
+
mock_run.return_value = MagicMock(stdout="+ test change")
|
|
170
|
+
mock_model.return_value = MockSystemLanguageModel()
|
|
171
|
+
mock_session.return_value = MockLanguageModelSession()
|
|
172
|
+
|
|
173
|
+
asyncio.run(commit_flow(quick=True))
|
|
174
|
+
|
|
175
|
+
mock_run.assert_called()
|
|
176
|
+
|
|
177
|
+
@patch('smartcommit.subprocess.run')
|
|
178
|
+
@patch('smartcommit.input', return_value='n')
|
|
179
|
+
@patch('smartcommit.fm.SystemLanguageModel')
|
|
180
|
+
@patch('smartcommit.fm.LanguageModelSession')
|
|
181
|
+
def test_commit_flow_reject(self, mock_session, mock_model, mock_input, mock_run):
|
|
182
|
+
mock_run.return_value = MagicMock(stdout="+ test change")
|
|
183
|
+
mock_model.return_value = MockSystemLanguageModel()
|
|
184
|
+
mock_session.return_value = MockLanguageModelSession()
|
|
185
|
+
|
|
186
|
+
asyncio.run(commit_flow(quick=True))
|
|
187
|
+
|
|
188
|
+
self.assertEqual(mock_run.call_count, 1)
|
|
189
|
+
|
|
190
|
+
@patch('smartcommit.subprocess.run')
|
|
191
|
+
@patch('smartcommit.input', side_effect=['r', 'y'])
|
|
192
|
+
@patch('smartcommit.fm.SystemLanguageModel')
|
|
193
|
+
@patch('smartcommit.fm.LanguageModelSession')
|
|
194
|
+
def test_commit_flow_retry(self, mock_session, mock_model, mock_input, mock_run):
|
|
195
|
+
mock_run.return_value = MagicMock(stdout="+ test change")
|
|
196
|
+
mock_model.return_value = MockSystemLanguageModel()
|
|
197
|
+
mock_session.return_value = MockLanguageModelSession()
|
|
198
|
+
|
|
199
|
+
asyncio.run(commit_flow(quick=True))
|
|
200
|
+
|
|
201
|
+
self.assertGreater(mock_run.call_count, 1)
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
class TestCLIParsing(unittest.TestCase):
|
|
205
|
+
def test_quick_flag_parsing(self):
|
|
206
|
+
with patch('sys.argv', ['smartcommit.py', '-q']):
|
|
207
|
+
from smartcommit import argparse
|
|
208
|
+
parser = argparse.ArgumentParser()
|
|
209
|
+
parser.add_argument('-q', '--quick', action='store_true')
|
|
210
|
+
args = parser.parse_args()
|
|
211
|
+
self.assertTrue(args.quick)
|
|
212
|
+
|
|
213
|
+
def test_context_flag_parsing(self):
|
|
214
|
+
with patch('sys.argv', ['smartcommit.py', '-c', 'test context']):
|
|
215
|
+
from smartcommit import argparse
|
|
216
|
+
parser = argparse.ArgumentParser()
|
|
217
|
+
parser.add_argument('-c', '--context', type=str)
|
|
218
|
+
args = parser.parse_args()
|
|
219
|
+
self.assertEqual(args.context, 'test context')
|
|
220
|
+
|
|
221
|
+
def test_combined_flags(self):
|
|
222
|
+
with patch('sys.argv', ['smartcommit.py', '-q', '-c', 'test']):
|
|
223
|
+
from smartcommit import argparse
|
|
224
|
+
parser = argparse.ArgumentParser()
|
|
225
|
+
parser.add_argument('-q', '--quick', action='store_true')
|
|
226
|
+
parser.add_argument('-c', '--context', type=str)
|
|
227
|
+
args = parser.parse_args()
|
|
228
|
+
self.assertTrue(args.quick)
|
|
229
|
+
self.assertEqual(args.context, 'test')
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
if __name__ == '__main__':
|
|
233
|
+
unittest.main()
|