ace-git-copilot 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.
- ace_git_copilot-0.1.0/.env.example +13 -0
- ace_git_copilot-0.1.0/.github/workflows/tests.yml +34 -0
- ace_git_copilot-0.1.0/.gitignore +45 -0
- ace_git_copilot-0.1.0/PKG-INFO +113 -0
- ace_git_copilot-0.1.0/README.md +93 -0
- ace_git_copilot-0.1.0/ace/__init__.py +1 -0
- ace_git_copilot-0.1.0/ace/ai/changelog_generator.py +70 -0
- ace_git_copilot-0.1.0/ace/ai/code_reviewer.py +81 -0
- ace_git_copilot-0.1.0/ace/ai/commit_generator.py +73 -0
- ace_git_copilot-0.1.0/ace/ai/conflict_resolver.py +115 -0
- ace_git_copilot-0.1.0/ace/ai/gitignore_generator.py +45 -0
- ace_git_copilot-0.1.0/ace/ai/history_analyzer.py +188 -0
- ace_git_copilot-0.1.0/ace/ai/intent_parser.py +65 -0
- ace_git_copilot-0.1.0/ace/ai/llm_factory.py +116 -0
- ace_git_copilot-0.1.0/ace/ai/pr_drafter.py +61 -0
- ace_git_copilot-0.1.0/ace/ai/prompts/changelog.py +30 -0
- ace_git_copilot-0.1.0/ace/ai/prompts/commit.py +74 -0
- ace_git_copilot-0.1.0/ace/ai/prompts/conflict.py +40 -0
- ace_git_copilot-0.1.0/ace/ai/prompts/explain.py +20 -0
- ace_git_copilot-0.1.0/ace/ai/prompts/ignore.py +19 -0
- ace_git_copilot-0.1.0/ace/ai/prompts/intent.py +111 -0
- ace_git_copilot-0.1.0/ace/ai/prompts/pr.py +29 -0
- ace_git_copilot-0.1.0/ace/ai/prompts/review.py +52 -0
- ace_git_copilot-0.1.0/ace/ai/prompts/search.py +18 -0
- ace_git_copilot-0.1.0/ace/ai/prompts/undo.py +47 -0
- ace_git_copilot-0.1.0/ace/cli.py +1191 -0
- ace_git_copilot-0.1.0/ace/core/config.py +129 -0
- ace_git_copilot-0.1.0/ace/core/context.py +172 -0
- ace_git_copilot-0.1.0/ace/core/git_ops.py +193 -0
- ace_git_copilot-0.1.0/ace/core/safety.py +110 -0
- ace_git_copilot-0.1.0/ace/ui/banner.py +64 -0
- ace_git_copilot-0.1.0/ace/ui/dashboard.py +200 -0
- ace_git_copilot-0.1.0/ace/ui/display.py +133 -0
- ace_git_copilot-0.1.0/ace/ui/prompts.py +69 -0
- ace_git_copilot-0.1.0/ace/ui/themes.py +22 -0
- ace_git_copilot-0.1.0/ace/utils/conflict_parser.py +62 -0
- ace_git_copilot-0.1.0/ace/utils/diff_parser.py +94 -0
- ace_git_copilot-0.1.0/ace/utils/json_utils.py +35 -0
- ace_git_copilot-0.1.0/pyproject.toml +31 -0
- ace_git_copilot-0.1.0/tests/conftest.py +27 -0
- ace_git_copilot-0.1.0/tests/test_changelog_generator.py +48 -0
- ace_git_copilot-0.1.0/tests/test_code_reviewer.py +88 -0
- ace_git_copilot-0.1.0/tests/test_conflict_resolver.py +93 -0
- ace_git_copilot-0.1.0/tests/test_diff_trimmer.py +31 -0
- ace_git_copilot-0.1.0/tests/test_git_ops.py +63 -0
- ace_git_copilot-0.1.0/tests/test_help.py +47 -0
- ace_git_copilot-0.1.0/tests/test_history_analyzer.py +67 -0
- ace_git_copilot-0.1.0/tests/test_ignore.py +38 -0
- ace_git_copilot-0.1.0/tests/test_intent_parser.py +75 -0
- ace_git_copilot-0.1.0/tests/test_llm_factory.py +51 -0
- ace_git_copilot-0.1.0/tests/test_pr_drafter.py +85 -0
- ace_git_copilot-0.1.0/tests/test_safety.py +46 -0
- ace_git_copilot-0.1.0/tests/test_search.py +44 -0
- ace_git_copilot-0.1.0/tests/test_undo.py +43 -0
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Ace AI Git Copilot Environment Variables
|
|
2
|
+
|
|
3
|
+
# AI Provider: 'nvidia' (default) or 'ollama' (offline)
|
|
4
|
+
ACE_PROVIDER=nvidia
|
|
5
|
+
|
|
6
|
+
# NVIDIA NIM Configuration
|
|
7
|
+
# Get your free key at https://build.nvidia.com/
|
|
8
|
+
NVIDIA_API_KEY=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
|
|
9
|
+
NVIDIA_MODEL=meta/llama-3.3-70b-instruct
|
|
10
|
+
|
|
11
|
+
# Ollama Configuration (Fallback/Offline)
|
|
12
|
+
OLLAMA_MODEL=qwen2.5-coder:7b
|
|
13
|
+
OLLAMA_URL=http://localhost:11434
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [ main, master ]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [ main, master ]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
test:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
strategy:
|
|
13
|
+
matrix:
|
|
14
|
+
python-version: ["3.11", "3.12"]
|
|
15
|
+
|
|
16
|
+
steps:
|
|
17
|
+
- uses: actions/checkout@v4
|
|
18
|
+
|
|
19
|
+
- name: Set up Python ${{ matrix.python-version }}
|
|
20
|
+
uses: actions/setup-python@v5
|
|
21
|
+
with:
|
|
22
|
+
python-version: ${{ matrix.python-version }}
|
|
23
|
+
cache: 'pip'
|
|
24
|
+
|
|
25
|
+
- name: Install dependencies
|
|
26
|
+
run: |
|
|
27
|
+
python -m pip install --upgrade pip
|
|
28
|
+
pip install .[dev]
|
|
29
|
+
|
|
30
|
+
- name: Lint with Ruff
|
|
31
|
+
run: ruff check
|
|
32
|
+
|
|
33
|
+
- name: Run tests with Pytest
|
|
34
|
+
run: pytest
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
*.so
|
|
6
|
+
.Python
|
|
7
|
+
build/
|
|
8
|
+
develop-eggs/
|
|
9
|
+
dist/
|
|
10
|
+
downloads/
|
|
11
|
+
eggs/
|
|
12
|
+
.eggs/
|
|
13
|
+
lib/
|
|
14
|
+
lib64/
|
|
15
|
+
parts/
|
|
16
|
+
sdist/
|
|
17
|
+
var/
|
|
18
|
+
wheels/
|
|
19
|
+
share/python-wheels/
|
|
20
|
+
*.egg-info/
|
|
21
|
+
.installed.cfg
|
|
22
|
+
*.egg
|
|
23
|
+
MANIFEST
|
|
24
|
+
|
|
25
|
+
# Virtual Environment
|
|
26
|
+
.venv/
|
|
27
|
+
venv/
|
|
28
|
+
ENV/
|
|
29
|
+
env/
|
|
30
|
+
|
|
31
|
+
# IDEs
|
|
32
|
+
.idea/
|
|
33
|
+
.vscode/
|
|
34
|
+
*.swp
|
|
35
|
+
*.swo
|
|
36
|
+
|
|
37
|
+
# Environment secrets
|
|
38
|
+
*.env
|
|
39
|
+
.env.local
|
|
40
|
+
|
|
41
|
+
# Testing / Coverage
|
|
42
|
+
.pytest_cache/
|
|
43
|
+
.coverage
|
|
44
|
+
htmlcov/
|
|
45
|
+
.cov/
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ace-git-copilot
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: AI-powered Git copilot — talk to Git in plain English
|
|
5
|
+
Requires-Python: >=3.11
|
|
6
|
+
Requires-Dist: click>=8.0
|
|
7
|
+
Requires-Dist: gitpython>=3.1
|
|
8
|
+
Requires-Dist: langchain-nvidia-ai-endpoints>=0.3
|
|
9
|
+
Requires-Dist: langchain-ollama>=0.3
|
|
10
|
+
Requires-Dist: langchain>=0.3
|
|
11
|
+
Requires-Dist: python-dotenv>=1.0
|
|
12
|
+
Requires-Dist: rich>=13.0
|
|
13
|
+
Requires-Dist: toml>=0.10.2
|
|
14
|
+
Requires-Dist: typer[all]>=0.12
|
|
15
|
+
Provides-Extra: dev
|
|
16
|
+
Requires-Dist: pytest; extra == 'dev'
|
|
17
|
+
Requires-Dist: pytest-cov; extra == 'dev'
|
|
18
|
+
Requires-Dist: ruff; extra == 'dev'
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
|
|
21
|
+
# Ace — AI-Powered Git Copilot
|
|
22
|
+
|
|
23
|
+
> Ace is an intelligent terminal tool that understands what you want to do with Git and does it for you. Talk to Git in plain English — Ace figures out the right git commands, explains what it's doing, and executes safely.
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## 🚀 Installation
|
|
28
|
+
|
|
29
|
+
Install in editable mode globally/user-wide:
|
|
30
|
+
```bash
|
|
31
|
+
python -m pip install --user -e D:\Ace
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
*Note: Make sure your Python scripts path (e.g., `AppData\Roaming\Python\Python3xx\Scripts`) is added to your Windows PATH environment variable.*
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## ⚙️ Configuration
|
|
39
|
+
|
|
40
|
+
Configure your AI provider (NVIDIA NIM or local Ollama) by running:
|
|
41
|
+
```bash
|
|
42
|
+
ace setup
|
|
43
|
+
```
|
|
44
|
+
This will save your preferences to `~/.ace/config.toml`.
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## 🛠️ Usage & Commands
|
|
49
|
+
|
|
50
|
+
### 🧠 Natural Language Input
|
|
51
|
+
Run anything you want to do directly:
|
|
52
|
+
```bash
|
|
53
|
+
ace "stage all changes"
|
|
54
|
+
ace "undo my last commit but keep changes"
|
|
55
|
+
ace "create feature branch named login"
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### 📋 Command Reference
|
|
59
|
+
|
|
60
|
+
| Command | Shorthand | Description |
|
|
61
|
+
|---------|-----------|-------------|
|
|
62
|
+
| `ace add <files>` | `ace stage` | Stage files (`git add`) for commit. |
|
|
63
|
+
| `ace commit` | — | Generate a smart Conventional Commit message and commit. |
|
|
64
|
+
| `ace review` | — | AI code review of staged/unstaged changes. |
|
|
65
|
+
| `ace resolve` | — | AI-assisted interactive merge conflict resolution. |
|
|
66
|
+
| `ace changelog` | — | Generate markdown release notes since the last tag. |
|
|
67
|
+
| `ace stats` | — | Enhanced visual repo overview, extension counts, and lines altered. |
|
|
68
|
+
| `ace explain <cmd>` | — | Explain Git commands or concepts in plain English. |
|
|
69
|
+
| `ace undo` | — | Context-aware smart undo (analyzes Git state to safely revert last action). |
|
|
70
|
+
| `ace pr` | — | Generate a markdown Pull Request description comparing branches. |
|
|
71
|
+
| `ace search <query>` | — | Semantic search across repository commits. |
|
|
72
|
+
| `ace ignore <query>` | — | Generate and append rules to `.gitignore`. |
|
|
73
|
+
| `ace dash` | — | Launch the interactive terminal dashboard TUI. |
|
|
74
|
+
| `ace config` | — | View the active configuration settings. |
|
|
75
|
+
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
## 📁 Codebase Directory & File Functions
|
|
79
|
+
|
|
80
|
+
Here is the list of files in the project and what each one does:
|
|
81
|
+
|
|
82
|
+
### 📍 Core CLI & Setup
|
|
83
|
+
* **`pyproject.toml`**: Configures the project, entry points (`ace = ace.cli:app`), dependencies (`typer`, `rich`, `gitpython`, `langchain`), and build systems.
|
|
84
|
+
* **`ace/cli.py`**: The entrypoint of the Typer application. Defines all CLI commands, parses command line flags, and routes calls.
|
|
85
|
+
|
|
86
|
+
### ⚙️ Core Modules (`ace/core/`)
|
|
87
|
+
* **`ace/core/config.py`**: Manages reading, writing, and parsing the user's TOML settings file (`config.toml`).
|
|
88
|
+
* **`ace/core/git_ops.py`**: Wraps the GitPython library to perform programmatic Git commands (e.g. status, diff, log, commit, push).
|
|
89
|
+
* **`ace/core/safety.py`**: Classifies Git commands into safety levels (Safe, Moderate, Destructive) and guards against destructive operations.
|
|
90
|
+
* **`ace/core/context.py`**: Collects repository-wide details (branch, diffs, untracked files) to provide context for AI requests.
|
|
91
|
+
|
|
92
|
+
### 🧠 AI Engine (`ace/ai/`)
|
|
93
|
+
* **`ace/ai/llm_factory.py`**: Instantiates ChatNVIDIA (cloud) or ChatOllama (local) depending on configuration settings.
|
|
94
|
+
* **`ace/ai/intent_parser.py`**: Translates the user's natural language input into Git commands.
|
|
95
|
+
* **`ace/ai/commit_generator.py`**: Analyzes staged diffs and generates Conventional Commit messages.
|
|
96
|
+
* **`ace/ai/code_reviewer.py`**: Analyzes unstaged or staged diffs to locate bugs, style flaws, security issues, and rate code.
|
|
97
|
+
* **`ace/ai/conflict_resolver.py`**: Parses file-level merge conflicts and suggests correct merges.
|
|
98
|
+
* **`ace/ai/changelog_generator.py`**: Collates commit messages and formats release logs.
|
|
99
|
+
* **`ace/ai/history_analyzer.py`**: Runs semantic searches across log files and computes repository overview metrics.
|
|
100
|
+
* **`ace/ai/pr_drafter.py`**: Drafts Pull Request titles and bodies comparing branch changes.
|
|
101
|
+
* **`ace/ai/gitignore_generator.py`**: Generates `.gitignore` syntax templates.
|
|
102
|
+
* **`ace/ai/prompts/`**: Houses all prompt templates for the LLMs (intent, commit, review, conflict, explain, undo, pr, search, ignore).
|
|
103
|
+
|
|
104
|
+
### 🎨 User Interface (`ace/ui/`)
|
|
105
|
+
* **`ace/ui/display.py`**: Directs Rich panel printouts, warning/error panels, and load spinners.
|
|
106
|
+
* **`ace/ui/themes.py`**: Houses colors, styling, and markdown rules.
|
|
107
|
+
* **`ace/ui/prompts.py`**: Handles keyboard selection options (e.g., commit/review prompts).
|
|
108
|
+
* **`ace/ui/dashboard.py`**: Implements the interactive terminal dashboard (TUI) showing branch states and command routing menus.
|
|
109
|
+
|
|
110
|
+
### 🔧 Utilities (`ace/utils/`)
|
|
111
|
+
* **`ace/utils/json_utils.py`**: Extracts and parses clean JSON structures from Markdown code blocks.
|
|
112
|
+
* **`ace/utils/diff_parser.py`**: Splits unified diff files into per-file chunks.
|
|
113
|
+
* **`ace/utils/conflict_parser.py`**: Parses git conflict blocks inside file lines.
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# Ace — AI-Powered Git Copilot
|
|
2
|
+
|
|
3
|
+
> Ace is an intelligent terminal tool that understands what you want to do with Git and does it for you. Talk to Git in plain English — Ace figures out the right git commands, explains what it's doing, and executes safely.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 🚀 Installation
|
|
8
|
+
|
|
9
|
+
Install in editable mode globally/user-wide:
|
|
10
|
+
```bash
|
|
11
|
+
python -m pip install --user -e D:\Ace
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
*Note: Make sure your Python scripts path (e.g., `AppData\Roaming\Python\Python3xx\Scripts`) is added to your Windows PATH environment variable.*
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## ⚙️ Configuration
|
|
19
|
+
|
|
20
|
+
Configure your AI provider (NVIDIA NIM or local Ollama) by running:
|
|
21
|
+
```bash
|
|
22
|
+
ace setup
|
|
23
|
+
```
|
|
24
|
+
This will save your preferences to `~/.ace/config.toml`.
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## 🛠️ Usage & Commands
|
|
29
|
+
|
|
30
|
+
### 🧠 Natural Language Input
|
|
31
|
+
Run anything you want to do directly:
|
|
32
|
+
```bash
|
|
33
|
+
ace "stage all changes"
|
|
34
|
+
ace "undo my last commit but keep changes"
|
|
35
|
+
ace "create feature branch named login"
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### 📋 Command Reference
|
|
39
|
+
|
|
40
|
+
| Command | Shorthand | Description |
|
|
41
|
+
|---------|-----------|-------------|
|
|
42
|
+
| `ace add <files>` | `ace stage` | Stage files (`git add`) for commit. |
|
|
43
|
+
| `ace commit` | — | Generate a smart Conventional Commit message and commit. |
|
|
44
|
+
| `ace review` | — | AI code review of staged/unstaged changes. |
|
|
45
|
+
| `ace resolve` | — | AI-assisted interactive merge conflict resolution. |
|
|
46
|
+
| `ace changelog` | — | Generate markdown release notes since the last tag. |
|
|
47
|
+
| `ace stats` | — | Enhanced visual repo overview, extension counts, and lines altered. |
|
|
48
|
+
| `ace explain <cmd>` | — | Explain Git commands or concepts in plain English. |
|
|
49
|
+
| `ace undo` | — | Context-aware smart undo (analyzes Git state to safely revert last action). |
|
|
50
|
+
| `ace pr` | — | Generate a markdown Pull Request description comparing branches. |
|
|
51
|
+
| `ace search <query>` | — | Semantic search across repository commits. |
|
|
52
|
+
| `ace ignore <query>` | — | Generate and append rules to `.gitignore`. |
|
|
53
|
+
| `ace dash` | — | Launch the interactive terminal dashboard TUI. |
|
|
54
|
+
| `ace config` | — | View the active configuration settings. |
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
## 📁 Codebase Directory & File Functions
|
|
59
|
+
|
|
60
|
+
Here is the list of files in the project and what each one does:
|
|
61
|
+
|
|
62
|
+
### 📍 Core CLI & Setup
|
|
63
|
+
* **`pyproject.toml`**: Configures the project, entry points (`ace = ace.cli:app`), dependencies (`typer`, `rich`, `gitpython`, `langchain`), and build systems.
|
|
64
|
+
* **`ace/cli.py`**: The entrypoint of the Typer application. Defines all CLI commands, parses command line flags, and routes calls.
|
|
65
|
+
|
|
66
|
+
### ⚙️ Core Modules (`ace/core/`)
|
|
67
|
+
* **`ace/core/config.py`**: Manages reading, writing, and parsing the user's TOML settings file (`config.toml`).
|
|
68
|
+
* **`ace/core/git_ops.py`**: Wraps the GitPython library to perform programmatic Git commands (e.g. status, diff, log, commit, push).
|
|
69
|
+
* **`ace/core/safety.py`**: Classifies Git commands into safety levels (Safe, Moderate, Destructive) and guards against destructive operations.
|
|
70
|
+
* **`ace/core/context.py`**: Collects repository-wide details (branch, diffs, untracked files) to provide context for AI requests.
|
|
71
|
+
|
|
72
|
+
### 🧠 AI Engine (`ace/ai/`)
|
|
73
|
+
* **`ace/ai/llm_factory.py`**: Instantiates ChatNVIDIA (cloud) or ChatOllama (local) depending on configuration settings.
|
|
74
|
+
* **`ace/ai/intent_parser.py`**: Translates the user's natural language input into Git commands.
|
|
75
|
+
* **`ace/ai/commit_generator.py`**: Analyzes staged diffs and generates Conventional Commit messages.
|
|
76
|
+
* **`ace/ai/code_reviewer.py`**: Analyzes unstaged or staged diffs to locate bugs, style flaws, security issues, and rate code.
|
|
77
|
+
* **`ace/ai/conflict_resolver.py`**: Parses file-level merge conflicts and suggests correct merges.
|
|
78
|
+
* **`ace/ai/changelog_generator.py`**: Collates commit messages and formats release logs.
|
|
79
|
+
* **`ace/ai/history_analyzer.py`**: Runs semantic searches across log files and computes repository overview metrics.
|
|
80
|
+
* **`ace/ai/pr_drafter.py`**: Drafts Pull Request titles and bodies comparing branch changes.
|
|
81
|
+
* **`ace/ai/gitignore_generator.py`**: Generates `.gitignore` syntax templates.
|
|
82
|
+
* **`ace/ai/prompts/`**: Houses all prompt templates for the LLMs (intent, commit, review, conflict, explain, undo, pr, search, ignore).
|
|
83
|
+
|
|
84
|
+
### 🎨 User Interface (`ace/ui/`)
|
|
85
|
+
* **`ace/ui/display.py`**: Directs Rich panel printouts, warning/error panels, and load spinners.
|
|
86
|
+
* **`ace/ui/themes.py`**: Houses colors, styling, and markdown rules.
|
|
87
|
+
* **`ace/ui/prompts.py`**: Handles keyboard selection options (e.g., commit/review prompts).
|
|
88
|
+
* **`ace/ui/dashboard.py`**: Implements the interactive terminal dashboard (TUI) showing branch states and command routing menus.
|
|
89
|
+
|
|
90
|
+
### 🔧 Utilities (`ace/utils/`)
|
|
91
|
+
* **`ace/utils/json_utils.py`**: Extracts and parses clean JSON structures from Markdown code blocks.
|
|
92
|
+
* **`ace/utils/diff_parser.py`**: Splits unified diff files into per-file chunks.
|
|
93
|
+
* **`ace/utils/conflict_parser.py`**: Parses git conflict blocks inside file lines.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
from typing import Optional
|
|
2
|
+
from langchain_core.messages import SystemMessage, HumanMessage
|
|
3
|
+
from ace.core.git_ops import GitOps
|
|
4
|
+
from ace.ai.llm_factory import get_llm
|
|
5
|
+
from ace.ai.prompts.changelog import CHANGELOG_SYSTEM_PROMPT, USER_PROMPT_TEMPLATE
|
|
6
|
+
|
|
7
|
+
class ChangelogGeneratorError(Exception):
|
|
8
|
+
"""Raised when changelog generation fails."""
|
|
9
|
+
pass
|
|
10
|
+
|
|
11
|
+
class ChangelogGenerator:
|
|
12
|
+
def __init__(self, git_ops: GitOps):
|
|
13
|
+
self.git_ops = git_ops
|
|
14
|
+
|
|
15
|
+
def get_commits_in_range(self, from_ref: Optional[str] = None, to_ref: Optional[str] = None) -> str:
|
|
16
|
+
"""
|
|
17
|
+
Retrieve formatted commit log between from_ref and to_ref.
|
|
18
|
+
If from_ref is None, attempts to find the latest tag.
|
|
19
|
+
If no latest tag exists, falls back to the last 30 commits.
|
|
20
|
+
"""
|
|
21
|
+
to_revision = to_ref or "HEAD"
|
|
22
|
+
from_revision = from_ref
|
|
23
|
+
|
|
24
|
+
# If from_ref is not provided, try to find the latest tag
|
|
25
|
+
if not from_revision:
|
|
26
|
+
try:
|
|
27
|
+
# git describe --tags --abbrev=0 to get latest tag
|
|
28
|
+
latest_tag = self.git_ops.execute("describe --tags --abbrev=0").strip()
|
|
29
|
+
from_revision = latest_tag
|
|
30
|
+
except Exception:
|
|
31
|
+
# No tags found
|
|
32
|
+
from_revision = None
|
|
33
|
+
|
|
34
|
+
log_args = ["log"]
|
|
35
|
+
if from_revision:
|
|
36
|
+
log_args.append(f"{from_revision}..{to_revision}")
|
|
37
|
+
else:
|
|
38
|
+
# Fallback to last 30 commits
|
|
39
|
+
log_args.extend(["-n", "30"])
|
|
40
|
+
|
|
41
|
+
# Format: hash|date|author|subject\nbody
|
|
42
|
+
log_args.append('--format=%H|%ad|%an|%s%n%b')
|
|
43
|
+
|
|
44
|
+
try:
|
|
45
|
+
cmd = " ".join(log_args)
|
|
46
|
+
return self.git_ops.execute(cmd).strip()
|
|
47
|
+
except Exception as e:
|
|
48
|
+
raise ChangelogGeneratorError(f"Failed to fetch commit log: {e}")
|
|
49
|
+
|
|
50
|
+
def generate_changelog(self, from_ref: Optional[str] = None, to_ref: Optional[str] = None, offline: bool = False) -> str:
|
|
51
|
+
"""
|
|
52
|
+
Retrieve commits and generate a release changelog in Markdown.
|
|
53
|
+
"""
|
|
54
|
+
commit_log = self.get_commits_in_range(from_ref, to_ref)
|
|
55
|
+
if not commit_log.strip():
|
|
56
|
+
return "No commits found in the specified range."
|
|
57
|
+
|
|
58
|
+
llm = get_llm(offline_override=offline)
|
|
59
|
+
|
|
60
|
+
user_prompt = USER_PROMPT_TEMPLATE.format(
|
|
61
|
+
commit_log=commit_log[:20000] # Cap log length to respect context window
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
messages = [
|
|
65
|
+
SystemMessage(content=CHANGELOG_SYSTEM_PROMPT),
|
|
66
|
+
HumanMessage(content=user_prompt)
|
|
67
|
+
]
|
|
68
|
+
|
|
69
|
+
response = llm.invoke(messages)
|
|
70
|
+
return response.content.strip()
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
from typing import Dict, Any, List, Tuple
|
|
2
|
+
from langchain_core.messages import SystemMessage, HumanMessage
|
|
3
|
+
from ace.core.git_ops import GitOps
|
|
4
|
+
from ace.utils.diff_parser import split_diff_by_file, trim_diff
|
|
5
|
+
from ace.ai.llm_factory import get_llm
|
|
6
|
+
from ace.ai.prompts.review import REVIEW_SYSTEM_PROMPT, USER_PROMPT_TEMPLATE
|
|
7
|
+
from ace.utils.json_utils import extract_json
|
|
8
|
+
|
|
9
|
+
class CodeReviewerError(Exception):
|
|
10
|
+
"""Raised when code review processing fails."""
|
|
11
|
+
pass
|
|
12
|
+
|
|
13
|
+
class CodeReviewer:
|
|
14
|
+
def __init__(self, git_ops: GitOps):
|
|
15
|
+
self.git_ops = git_ops
|
|
16
|
+
|
|
17
|
+
def review_diff(self, diff_text: str, offline: bool = False) -> Tuple[List[Dict[str, Any]], float]:
|
|
18
|
+
"""
|
|
19
|
+
Perform a code review on a raw diff string.
|
|
20
|
+
|
|
21
|
+
Returns a tuple of (findings, overall_score).
|
|
22
|
+
"""
|
|
23
|
+
if not diff_text.strip():
|
|
24
|
+
return [], 10.0
|
|
25
|
+
|
|
26
|
+
# Split diff by file
|
|
27
|
+
file_diffs = split_diff_by_file(diff_text)
|
|
28
|
+
if not file_diffs:
|
|
29
|
+
return [], 10.0
|
|
30
|
+
|
|
31
|
+
all_findings = []
|
|
32
|
+
scores = []
|
|
33
|
+
llm = get_llm(offline_override=offline)
|
|
34
|
+
|
|
35
|
+
for filename, diff_content in file_diffs.items():
|
|
36
|
+
# Skip binary diffs or very small metadata updates
|
|
37
|
+
if "Binary files" in diff_content or len(diff_content.strip()) < 20:
|
|
38
|
+
continue
|
|
39
|
+
|
|
40
|
+
user_prompt = USER_PROMPT_TEMPLATE.format(
|
|
41
|
+
filename=filename,
|
|
42
|
+
diff_content=trim_diff(diff_content, max_chars=15000) # Cap file diff size
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
messages = [
|
|
46
|
+
SystemMessage(content=REVIEW_SYSTEM_PROMPT),
|
|
47
|
+
HumanMessage(content=user_prompt)
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
try:
|
|
51
|
+
response = llm.invoke(messages)
|
|
52
|
+
parsed = extract_json(response.content)
|
|
53
|
+
|
|
54
|
+
# Extract score and findings
|
|
55
|
+
score = float(parsed.get("score", 10.0))
|
|
56
|
+
scores.append(score)
|
|
57
|
+
|
|
58
|
+
findings = parsed.get("findings", [])
|
|
59
|
+
if isinstance(findings, list):
|
|
60
|
+
for finding in findings:
|
|
61
|
+
# Annotate with the filename
|
|
62
|
+
finding["file"] = filename
|
|
63
|
+
all_findings.append(finding)
|
|
64
|
+
except Exception as e:
|
|
65
|
+
# Log or handle exception per file, but don't crash the whole review
|
|
66
|
+
# We can add a fallback warning finding
|
|
67
|
+
all_findings.append({
|
|
68
|
+
"file": filename,
|
|
69
|
+
"category": "suggestion",
|
|
70
|
+
"severity": "info",
|
|
71
|
+
"line": None,
|
|
72
|
+
"description": f"AI review failed for this file: {e}",
|
|
73
|
+
"fix": None
|
|
74
|
+
})
|
|
75
|
+
scores.append(10.0)
|
|
76
|
+
|
|
77
|
+
overall_score = sum(scores) / len(scores) if scores else 10.0
|
|
78
|
+
# Round overall score
|
|
79
|
+
overall_score = round(overall_score, 1)
|
|
80
|
+
|
|
81
|
+
return all_findings, overall_score
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
from langchain_core.messages import SystemMessage, HumanMessage
|
|
2
|
+
from ace.core.git_ops import GitOps
|
|
3
|
+
from ace.core.context import RepoContext
|
|
4
|
+
from ace.ai.llm_factory import get_llm
|
|
5
|
+
from ace.utils.diff_parser import trim_diff
|
|
6
|
+
from ace.ai.prompts.commit import (
|
|
7
|
+
CONVENTIONAL_COMMIT_SYSTEM_PROMPT,
|
|
8
|
+
SIMPLE_COMMIT_SYSTEM_PROMPT,
|
|
9
|
+
DETAILED_COMMIT_SYSTEM_PROMPT,
|
|
10
|
+
USER_PROMPT_TEMPLATE,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
class NoStagedChangesError(Exception):
|
|
14
|
+
"""Raised when trying to generate a commit message but no changes are staged."""
|
|
15
|
+
pass
|
|
16
|
+
|
|
17
|
+
class CommitGenerator:
|
|
18
|
+
def __init__(self, git_ops: GitOps):
|
|
19
|
+
self.git_ops = git_ops
|
|
20
|
+
self.context_builder = RepoContext(git_ops)
|
|
21
|
+
|
|
22
|
+
def generate_message(self, format_type: str = "conventional", offline: bool = False) -> str:
|
|
23
|
+
"""
|
|
24
|
+
Analyze staged changes and generate a commit message using the configured AI.
|
|
25
|
+
"""
|
|
26
|
+
# Ensure we have staged changes
|
|
27
|
+
status = self.git_ops.get_status()
|
|
28
|
+
if not status["staged"]:
|
|
29
|
+
raise NoStagedChangesError("No changes are staged for commit. Stage files first using 'git add'.")
|
|
30
|
+
|
|
31
|
+
staged_diff = self.git_ops.get_staged_diff()
|
|
32
|
+
if not staged_diff.strip():
|
|
33
|
+
# Sometimes status has staged but diff is empty (e.g. only file permissions or empty files)
|
|
34
|
+
raise NoStagedChangesError("Staged diff is empty. Cannot generate commit message.")
|
|
35
|
+
|
|
36
|
+
# Format context
|
|
37
|
+
repo_context = self.context_builder.format_context_for_prompt()
|
|
38
|
+
|
|
39
|
+
# Select system prompt based on format
|
|
40
|
+
if format_type == "conventional":
|
|
41
|
+
system_prompt = CONVENTIONAL_COMMIT_SYSTEM_PROMPT
|
|
42
|
+
elif format_type == "simple":
|
|
43
|
+
system_prompt = SIMPLE_COMMIT_SYSTEM_PROMPT
|
|
44
|
+
elif format_type == "detailed":
|
|
45
|
+
system_prompt = DETAILED_COMMIT_SYSTEM_PROMPT
|
|
46
|
+
else:
|
|
47
|
+
system_prompt = CONVENTIONAL_COMMIT_SYSTEM_PROMPT
|
|
48
|
+
|
|
49
|
+
user_prompt = USER_PROMPT_TEMPLATE.format(
|
|
50
|
+
repo_context=repo_context,
|
|
51
|
+
staged_diff=trim_diff(staged_diff, max_chars=25000) # Cap diff to avoid context window limit
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
messages = [
|
|
55
|
+
SystemMessage(content=system_prompt),
|
|
56
|
+
HumanMessage(content=user_prompt)
|
|
57
|
+
]
|
|
58
|
+
|
|
59
|
+
# Get LLM and run inference
|
|
60
|
+
llm = get_llm(offline_override=offline)
|
|
61
|
+
response = llm.invoke(messages)
|
|
62
|
+
|
|
63
|
+
# Clean response (remove extra leading/trailing whitespace or markdown fences)
|
|
64
|
+
message = response.content.strip()
|
|
65
|
+
if message.startswith("```"):
|
|
66
|
+
lines = message.splitlines()
|
|
67
|
+
if lines[0].startswith("```"):
|
|
68
|
+
lines = lines[1:]
|
|
69
|
+
if lines and lines[-1].startswith("```"):
|
|
70
|
+
lines = lines[:-1]
|
|
71
|
+
message = "\n".join(lines).strip()
|
|
72
|
+
|
|
73
|
+
return message
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from typing import Dict, Any, List, Tuple
|
|
3
|
+
from langchain_core.messages import SystemMessage, HumanMessage
|
|
4
|
+
from ace.core.git_ops import GitOps
|
|
5
|
+
from ace.utils.conflict_parser import parse_conflict_file
|
|
6
|
+
from ace.ai.llm_factory import get_llm
|
|
7
|
+
from ace.ai.prompts.conflict import CONFLICT_SYSTEM_PROMPT, USER_PROMPT_TEMPLATE
|
|
8
|
+
from ace.utils.json_utils import extract_json
|
|
9
|
+
|
|
10
|
+
class ConflictResolverError(Exception):
|
|
11
|
+
"""Raised when conflict resolution fails."""
|
|
12
|
+
pass
|
|
13
|
+
|
|
14
|
+
class ConflictResolver:
|
|
15
|
+
def __init__(self, git_ops: GitOps):
|
|
16
|
+
self.git_ops = git_ops
|
|
17
|
+
|
|
18
|
+
def get_suggestions(self, file_path: str, offline: bool = False) -> List[Dict[str, Any]]:
|
|
19
|
+
"""
|
|
20
|
+
Scan a file for conflicts and return AI resolution suggestions for each.
|
|
21
|
+
|
|
22
|
+
Returns a list of dicts:
|
|
23
|
+
[
|
|
24
|
+
{
|
|
25
|
+
"full_block": str,
|
|
26
|
+
"head": str,
|
|
27
|
+
"incoming": str,
|
|
28
|
+
"suggested_merged": str,
|
|
29
|
+
"explanation": str
|
|
30
|
+
}
|
|
31
|
+
]
|
|
32
|
+
"""
|
|
33
|
+
full_path = Path(self.git_ops.working_dir) / file_path
|
|
34
|
+
if not full_path.exists():
|
|
35
|
+
raise FileNotFoundError(f"File not found: {file_path}")
|
|
36
|
+
|
|
37
|
+
try:
|
|
38
|
+
content = full_path.read_text(encoding="utf-8")
|
|
39
|
+
except Exception as e:
|
|
40
|
+
raise ConflictResolverError(f"Failed to read file {file_path}: {e}")
|
|
41
|
+
|
|
42
|
+
blocks = parse_conflict_file(content)
|
|
43
|
+
if not blocks:
|
|
44
|
+
return []
|
|
45
|
+
|
|
46
|
+
llm = get_llm(offline_override=offline)
|
|
47
|
+
suggestions = []
|
|
48
|
+
|
|
49
|
+
for block in blocks:
|
|
50
|
+
sys_prompt = CONFLICT_SYSTEM_PROMPT.format(filename=file_path)
|
|
51
|
+
usr_prompt = USER_PROMPT_TEMPLATE.format(
|
|
52
|
+
head_content=block["head"],
|
|
53
|
+
incoming_content=block["incoming"]
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
messages = [
|
|
57
|
+
SystemMessage(content=sys_prompt),
|
|
58
|
+
HumanMessage(content=usr_prompt)
|
|
59
|
+
]
|
|
60
|
+
|
|
61
|
+
try:
|
|
62
|
+
response = llm.invoke(messages)
|
|
63
|
+
parsed = extract_json(response.content)
|
|
64
|
+
|
|
65
|
+
suggestions.append({
|
|
66
|
+
"full_block": block["full_block"],
|
|
67
|
+
"head": block["head"],
|
|
68
|
+
"incoming": block["incoming"],
|
|
69
|
+
"suggested_merged": parsed.get("merged_content", block["head"]), # Fallback to head
|
|
70
|
+
"explanation": parsed.get("explanation", "AI suggested resolution.")
|
|
71
|
+
})
|
|
72
|
+
except Exception as e:
|
|
73
|
+
# Fallback in case of AI failures
|
|
74
|
+
suggestions.append({
|
|
75
|
+
"full_block": block["full_block"],
|
|
76
|
+
"head": block["head"],
|
|
77
|
+
"incoming": block["incoming"],
|
|
78
|
+
"suggested_merged": block["head"], # Keep head as fallback
|
|
79
|
+
"explanation": f"AI suggestion failed: {e}. Keeping local branch version as default suggestion."
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
return suggestions
|
|
83
|
+
|
|
84
|
+
def apply_resolution(self, file_path: str, block_replacements: List[Tuple[str, str]]) -> None:
|
|
85
|
+
"""
|
|
86
|
+
Apply resolutions to a conflicted file.
|
|
87
|
+
|
|
88
|
+
block_replacements: List of tuples (full_conflict_block, replacement_content)
|
|
89
|
+
"""
|
|
90
|
+
full_path = Path(self.git_ops.working_dir) / file_path
|
|
91
|
+
if not full_path.exists():
|
|
92
|
+
raise FileNotFoundError(f"File not found: {file_path}")
|
|
93
|
+
|
|
94
|
+
content = full_path.read_text(encoding="utf-8")
|
|
95
|
+
|
|
96
|
+
for full_block, replacement in block_replacements:
|
|
97
|
+
if full_block in content:
|
|
98
|
+
content = content.replace(full_block, replacement)
|
|
99
|
+
else:
|
|
100
|
+
# Try with normalized line endings
|
|
101
|
+
norm_block = full_block.replace("\r\n", "\n")
|
|
102
|
+
norm_content = content.replace("\r\n", "\n")
|
|
103
|
+
if norm_block in norm_content:
|
|
104
|
+
norm_content = norm_content.replace(norm_block, replacement)
|
|
105
|
+
# Restore Windows line endings if they were originally present
|
|
106
|
+
if "\r\n" in content:
|
|
107
|
+
content = norm_content.replace("\n", "\r\n")
|
|
108
|
+
else:
|
|
109
|
+
content = norm_content
|
|
110
|
+
else:
|
|
111
|
+
raise ConflictResolverError(
|
|
112
|
+
"Conflict block not found in file. Has it been edited already?"
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
full_path.write_text(content, encoding="utf-8")
|