gitpr-cli 0.0.29__tar.gz → 0.0.31__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.
- {gitpr_cli-0.0.29 → gitpr_cli-0.0.31}/PKG-INFO +26 -2
- {gitpr_cli-0.0.29 → gitpr_cli-0.0.31}/README.md +24 -0
- {gitpr_cli-0.0.29 → gitpr_cli-0.0.31}/gitpr_cli.egg-info/PKG-INFO +26 -2
- {gitpr_cli-0.0.29 → gitpr_cli-0.0.31}/gitpr_cli.egg-info/SOURCES.txt +3 -0
- {gitpr_cli-0.0.29 → gitpr_cli-0.0.31}/pyproject.toml +2 -2
- {gitpr_cli-0.0.29 → gitpr_cli-0.0.31}/src/ai_providers.py +23 -0
- {gitpr_cli-0.0.29 → gitpr_cli-0.0.31}/src/cache.py +17 -1
- {gitpr_cli-0.0.29 → gitpr_cli-0.0.31}/src/config.py +30 -2
- {gitpr_cli-0.0.29 → gitpr_cli-0.0.31}/src/core.py +54 -8
- {gitpr_cli-0.0.29 → gitpr_cli-0.0.31}/src/issue_engine.py +24 -4
- {gitpr_cli-0.0.29 → gitpr_cli-0.0.31}/src/main.py +108 -16
- gitpr_cli-0.0.31/src/metrics.py +588 -0
- {gitpr_cli-0.0.29 → gitpr_cli-0.0.31}/src/spinner.py +6 -6
- gitpr_cli-0.0.31/src/tui_issue.py +122 -0
- {gitpr_cli-0.0.29 → gitpr_cli-0.0.31}/src/ui/issue_app.py +16 -4
- gitpr_cli-0.0.31/src/ui/metrics_app.py +434 -0
- {gitpr_cli-0.0.29 → gitpr_cli-0.0.31}/src/updater.py +2 -2
- gitpr_cli-0.0.31/tests/test_metrics.py +833 -0
- gitpr_cli-0.0.29/src/tui_issue.py +0 -37
- {gitpr_cli-0.0.29 → gitpr_cli-0.0.31}/LICENSE +0 -0
- {gitpr_cli-0.0.29 → gitpr_cli-0.0.31}/gitpr_cli.egg-info/dependency_links.txt +0 -0
- {gitpr_cli-0.0.29 → gitpr_cli-0.0.31}/gitpr_cli.egg-info/entry_points.txt +0 -0
- {gitpr_cli-0.0.29 → gitpr_cli-0.0.31}/gitpr_cli.egg-info/requires.txt +0 -0
- {gitpr_cli-0.0.29 → gitpr_cli-0.0.31}/gitpr_cli.egg-info/top_level.txt +0 -0
- {gitpr_cli-0.0.29 → gitpr_cli-0.0.31}/setup.cfg +0 -0
- {gitpr_cli-0.0.29 → gitpr_cli-0.0.31}/src/__init__.py +0 -0
- {gitpr_cli-0.0.29 → gitpr_cli-0.0.31}/src/blame_engine.py +0 -0
- {gitpr_cli-0.0.29 → gitpr_cli-0.0.31}/src/chat_memory.py +0 -0
- {gitpr_cli-0.0.29 → gitpr_cli-0.0.31}/src/i18n.py +0 -0
- {gitpr_cli-0.0.29 → gitpr_cli-0.0.31}/src/linter_engine.py +0 -0
- {gitpr_cli-0.0.29 → gitpr_cli-0.0.31}/src/mcp_server.py +0 -0
- {gitpr_cli-0.0.29 → gitpr_cli-0.0.31}/src/security.py +0 -0
- {gitpr_cli-0.0.29 → gitpr_cli-0.0.31}/src/ui/__init__.py +0 -0
- {gitpr_cli-0.0.29 → gitpr_cli-0.0.31}/src/ui/chat_app.py +0 -0
- {gitpr_cli-0.0.29 → gitpr_cli-0.0.31}/src/ui/help_screen.py +0 -0
- {gitpr_cli-0.0.29 → gitpr_cli-0.0.31}/tests/test_chat_backend.py +0 -0
- {gitpr_cli-0.0.29 → gitpr_cli-0.0.31}/tests/test_core.py +0 -0
- {gitpr_cli-0.0.29 → gitpr_cli-0.0.31}/tests/test_install_wizard.py +0 -0
- {gitpr_cli-0.0.29 → gitpr_cli-0.0.31}/tests/test_mcp_prompts.py +0 -0
- {gitpr_cli-0.0.29 → gitpr_cli-0.0.31}/tests/test_mcp_server.py +0 -0
- {gitpr_cli-0.0.29 → gitpr_cli-0.0.31}/tests/test_pre_save.py +0 -0
- {gitpr_cli-0.0.29 → gitpr_cli-0.0.31}/tests/test_skill_command.py +0 -0
- {gitpr_cli-0.0.29 → gitpr_cli-0.0.31}/tests/test_smart_excludes.py +0 -0
- {gitpr_cli-0.0.29 → gitpr_cli-0.0.31}/tests/test_thinking_words.py +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: gitpr-cli
|
|
3
|
-
Version: 0.0.
|
|
4
|
-
Summary:
|
|
3
|
+
Version: 0.0.31
|
|
4
|
+
Summary: AI-powered PR automation, commit messages, and code review (Gemini, DeepSeek, and Ollama)
|
|
5
5
|
Author-email: Natan Fiuza <contato@natanfiuza.dev.br>
|
|
6
6
|
Requires-Python: >=3.10
|
|
7
7
|
Description-Content-Type: text/markdown
|
|
@@ -28,6 +28,29 @@ GitPR CLI is a command-line automation tool that uses **Google Gemini** and **De
|
|
|
28
28
|
|
|
29
29
|
🌐 **Website:** [gitpr.natanfiuza.dev.br](https://gitpr.natanfiuza.dev.br/) · 📂 **Repository:** [github.com/natanfiuza/gitpr](https://github.com/natanfiuza/gitpr)
|
|
30
30
|
|
|
31
|
+
----
|
|
32
|
+
|
|
33
|
+
## ⚡ **Quick Start**
|
|
34
|
+
|
|
35
|
+
### **1. Installation via PyPI**
|
|
36
|
+
|
|
37
|
+
Install GitPR CLI using `pip`:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
pip install gitpr-cli
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### **2. Initializing in a Repository**
|
|
44
|
+
|
|
45
|
+
To set up GitPR in a folder of a new repository, run:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
gitpr --install
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
> **Guided Setup:** Guided configuration that downloads skill templates, installs Git Hooks, configures MCP for your editors, and verifies the API key for your AI provider.
|
|
52
|
+
> 📖 **Full Documentation:** [https://gitpr.natanfiuza.dev.br/docs/install-wizard?lang=en_us](https://gitpr.natanfiuza.dev.br/docs/install-wizard?lang=en_us)
|
|
53
|
+
|
|
31
54
|
## **🛠️ Technologies and Libraries Used**
|
|
32
55
|
|
|
33
56
|
This project was developed in Python and uses the following main libraries:
|
|
@@ -311,6 +334,7 @@ If you want to implement GitPR as an automated quality barrier in your team, che
|
|
|
311
334
|
* [**MCP Integration**](https://github.com/natanfiuza/gitpr/blob/main/docs/mcp-integration.md) — Connect GitPR to VS Code, Cursor, and Claude Desktop via Model Context Protocol.
|
|
312
335
|
* [**MCP Prompts**](https://github.com/natanfiuza/gitpr/blob/main/docs/mcp-prompts.md) — Pre-built message templates (7 prompts, 35 language variants) for common GitPR workflows in your editor's AI chat.
|
|
313
336
|
* [**MCP Tool Annotations**](https://github.com/natanfiuza/gitpr/blob/main/docs/mcp-annotations.md) — IDE integration hints (`readOnlyHint`, `destructiveHint`) for smarter UI behavior and safer tool execution.
|
|
337
|
+
* [**Metrics & Telemetry**](https://github.com/natanfiuza/gitpr/blob/main/docs/metricas-telemetria.md) — Local offline analytics for team usage metrics, exportable CSV reports, and interactive TUI dashboard.
|
|
314
338
|
|
|
315
339
|
## ⚡ Local Cache System (Quota Savings)
|
|
316
340
|
|
|
@@ -9,6 +9,29 @@ GitPR CLI is a command-line automation tool that uses **Google Gemini** and **De
|
|
|
9
9
|
|
|
10
10
|
🌐 **Website:** [gitpr.natanfiuza.dev.br](https://gitpr.natanfiuza.dev.br/) · 📂 **Repository:** [github.com/natanfiuza/gitpr](https://github.com/natanfiuza/gitpr)
|
|
11
11
|
|
|
12
|
+
----
|
|
13
|
+
|
|
14
|
+
## ⚡ **Quick Start**
|
|
15
|
+
|
|
16
|
+
### **1. Installation via PyPI**
|
|
17
|
+
|
|
18
|
+
Install GitPR CLI using `pip`:
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
pip install gitpr-cli
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
### **2. Initializing in a Repository**
|
|
25
|
+
|
|
26
|
+
To set up GitPR in a folder of a new repository, run:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
gitpr --install
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
> **Guided Setup:** Guided configuration that downloads skill templates, installs Git Hooks, configures MCP for your editors, and verifies the API key for your AI provider.
|
|
33
|
+
> 📖 **Full Documentation:** [https://gitpr.natanfiuza.dev.br/docs/install-wizard?lang=en_us](https://gitpr.natanfiuza.dev.br/docs/install-wizard?lang=en_us)
|
|
34
|
+
|
|
12
35
|
## **🛠️ Technologies and Libraries Used**
|
|
13
36
|
|
|
14
37
|
This project was developed in Python and uses the following main libraries:
|
|
@@ -292,6 +315,7 @@ If you want to implement GitPR as an automated quality barrier in your team, che
|
|
|
292
315
|
* [**MCP Integration**](https://github.com/natanfiuza/gitpr/blob/main/docs/mcp-integration.md) — Connect GitPR to VS Code, Cursor, and Claude Desktop via Model Context Protocol.
|
|
293
316
|
* [**MCP Prompts**](https://github.com/natanfiuza/gitpr/blob/main/docs/mcp-prompts.md) — Pre-built message templates (7 prompts, 35 language variants) for common GitPR workflows in your editor's AI chat.
|
|
294
317
|
* [**MCP Tool Annotations**](https://github.com/natanfiuza/gitpr/blob/main/docs/mcp-annotations.md) — IDE integration hints (`readOnlyHint`, `destructiveHint`) for smarter UI behavior and safer tool execution.
|
|
318
|
+
* [**Metrics & Telemetry**](https://github.com/natanfiuza/gitpr/blob/main/docs/metricas-telemetria.md) — Local offline analytics for team usage metrics, exportable CSV reports, and interactive TUI dashboard.
|
|
295
319
|
|
|
296
320
|
## ⚡ Local Cache System (Quota Savings)
|
|
297
321
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: gitpr-cli
|
|
3
|
-
Version: 0.0.
|
|
4
|
-
Summary:
|
|
3
|
+
Version: 0.0.31
|
|
4
|
+
Summary: AI-powered PR automation, commit messages, and code review (Gemini, DeepSeek, and Ollama)
|
|
5
5
|
Author-email: Natan Fiuza <contato@natanfiuza.dev.br>
|
|
6
6
|
Requires-Python: >=3.10
|
|
7
7
|
Description-Content-Type: text/markdown
|
|
@@ -28,6 +28,29 @@ GitPR CLI is a command-line automation tool that uses **Google Gemini** and **De
|
|
|
28
28
|
|
|
29
29
|
🌐 **Website:** [gitpr.natanfiuza.dev.br](https://gitpr.natanfiuza.dev.br/) · 📂 **Repository:** [github.com/natanfiuza/gitpr](https://github.com/natanfiuza/gitpr)
|
|
30
30
|
|
|
31
|
+
----
|
|
32
|
+
|
|
33
|
+
## ⚡ **Quick Start**
|
|
34
|
+
|
|
35
|
+
### **1. Installation via PyPI**
|
|
36
|
+
|
|
37
|
+
Install GitPR CLI using `pip`:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
pip install gitpr-cli
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### **2. Initializing in a Repository**
|
|
44
|
+
|
|
45
|
+
To set up GitPR in a folder of a new repository, run:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
gitpr --install
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
> **Guided Setup:** Guided configuration that downloads skill templates, installs Git Hooks, configures MCP for your editors, and verifies the API key for your AI provider.
|
|
52
|
+
> 📖 **Full Documentation:** [https://gitpr.natanfiuza.dev.br/docs/install-wizard?lang=en_us](https://gitpr.natanfiuza.dev.br/docs/install-wizard?lang=en_us)
|
|
53
|
+
|
|
31
54
|
## **🛠️ Technologies and Libraries Used**
|
|
32
55
|
|
|
33
56
|
This project was developed in Python and uses the following main libraries:
|
|
@@ -311,6 +334,7 @@ If you want to implement GitPR as an automated quality barrier in your team, che
|
|
|
311
334
|
* [**MCP Integration**](https://github.com/natanfiuza/gitpr/blob/main/docs/mcp-integration.md) — Connect GitPR to VS Code, Cursor, and Claude Desktop via Model Context Protocol.
|
|
312
335
|
* [**MCP Prompts**](https://github.com/natanfiuza/gitpr/blob/main/docs/mcp-prompts.md) — Pre-built message templates (7 prompts, 35 language variants) for common GitPR workflows in your editor's AI chat.
|
|
313
336
|
* [**MCP Tool Annotations**](https://github.com/natanfiuza/gitpr/blob/main/docs/mcp-annotations.md) — IDE integration hints (`readOnlyHint`, `destructiveHint`) for smarter UI behavior and safer tool execution.
|
|
337
|
+
* [**Metrics & Telemetry**](https://github.com/natanfiuza/gitpr/blob/main/docs/metricas-telemetria.md) — Local offline analytics for team usage metrics, exportable CSV reports, and interactive TUI dashboard.
|
|
314
338
|
|
|
315
339
|
## ⚡ Local Cache System (Quota Savings)
|
|
316
340
|
|
|
@@ -19,6 +19,7 @@ src/issue_engine.py
|
|
|
19
19
|
src/linter_engine.py
|
|
20
20
|
src/main.py
|
|
21
21
|
src/mcp_server.py
|
|
22
|
+
src/metrics.py
|
|
22
23
|
src/security.py
|
|
23
24
|
src/spinner.py
|
|
24
25
|
src/tui_issue.py
|
|
@@ -27,11 +28,13 @@ src/ui/__init__.py
|
|
|
27
28
|
src/ui/chat_app.py
|
|
28
29
|
src/ui/help_screen.py
|
|
29
30
|
src/ui/issue_app.py
|
|
31
|
+
src/ui/metrics_app.py
|
|
30
32
|
tests/test_chat_backend.py
|
|
31
33
|
tests/test_core.py
|
|
32
34
|
tests/test_install_wizard.py
|
|
33
35
|
tests/test_mcp_prompts.py
|
|
34
36
|
tests/test_mcp_server.py
|
|
37
|
+
tests/test_metrics.py
|
|
35
38
|
tests/test_pre_save.py
|
|
36
39
|
tests/test_skill_command.py
|
|
37
40
|
tests/test_smart_excludes.py
|
|
@@ -4,8 +4,8 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "gitpr-cli"
|
|
7
|
-
version = "0.0.
|
|
8
|
-
description = "
|
|
7
|
+
version = "0.0.31"
|
|
8
|
+
description = "AI-powered PR automation, commit messages, and code review (Gemini, DeepSeek, and Ollama)"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
11
11
|
authors = [
|
|
@@ -73,9 +73,12 @@ def call_ai_model(provider, api_key, api_model, prompt, system_instruction, quie
|
|
|
73
73
|
spinner = Spinner(quiet=quiet)
|
|
74
74
|
spinner.start()
|
|
75
75
|
|
|
76
|
+
start_time = time.perf_counter()
|
|
77
|
+
|
|
76
78
|
try:
|
|
77
79
|
for attempt in range(1, max_retries + 1):
|
|
78
80
|
try:
|
|
81
|
+
meta_raw = {}
|
|
79
82
|
if provider == "gemini":
|
|
80
83
|
client = genai.Client(api_key=api_key)
|
|
81
84
|
response = client.models.generate_content(
|
|
@@ -90,6 +93,13 @@ def call_ai_model(provider, api_key, api_model, prompt, system_instruction, quie
|
|
|
90
93
|
}
|
|
91
94
|
)
|
|
92
95
|
result_text = response.text
|
|
96
|
+
|
|
97
|
+
if hasattr(response, 'usage_metadata') and response.usage_metadata:
|
|
98
|
+
meta_raw = {
|
|
99
|
+
"prompt_tokens": getattr(response.usage_metadata, 'prompt_token_count', 0),
|
|
100
|
+
"completion_tokens": getattr(response.usage_metadata, 'candidates_token_count', 0),
|
|
101
|
+
"total_tokens": getattr(response.usage_metadata, 'total_token_count', 0)
|
|
102
|
+
}
|
|
93
103
|
|
|
94
104
|
elif provider in ["deepseek", "ollama"]:
|
|
95
105
|
# DeepSeek and Ollama are 100% compatible with the OpenAI library.
|
|
@@ -106,6 +116,13 @@ def call_ai_model(provider, api_key, api_model, prompt, system_instruction, quie
|
|
|
106
116
|
temperature=0.0
|
|
107
117
|
)
|
|
108
118
|
result_text = response.choices[0].message.content
|
|
119
|
+
|
|
120
|
+
if hasattr(response, 'usage') and response.usage:
|
|
121
|
+
meta_raw = {
|
|
122
|
+
"prompt_tokens": getattr(response.usage, 'prompt_tokens', 0),
|
|
123
|
+
"completion_tokens": getattr(response.usage, 'completion_tokens', 0),
|
|
124
|
+
"total_tokens": getattr(response.usage, 'total_tokens', 0)
|
|
125
|
+
}
|
|
109
126
|
|
|
110
127
|
else:
|
|
111
128
|
spinner.stop()
|
|
@@ -119,6 +136,12 @@ def call_ai_model(provider, api_key, api_model, prompt, system_instruction, quie
|
|
|
119
136
|
if isinstance(result_json, list):
|
|
120
137
|
result_json = result_json[0] if result_json else {}
|
|
121
138
|
|
|
139
|
+
# Inject telemetry metadata silently into the response dictionary
|
|
140
|
+
meta_raw["duration_ms"] = int((time.perf_counter() - start_time) * 1000)
|
|
141
|
+
meta_raw["provider"] = provider
|
|
142
|
+
meta_raw["model"] = api_model
|
|
143
|
+
result_json["_telemetry_meta"] = meta_raw
|
|
144
|
+
|
|
122
145
|
spinner.stop()
|
|
123
146
|
return result_json
|
|
124
147
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import os
|
|
2
2
|
import json
|
|
3
3
|
import hashlib
|
|
4
|
+
import subprocess
|
|
4
5
|
from datetime import datetime
|
|
5
6
|
from pathlib import Path
|
|
6
7
|
|
|
@@ -13,6 +14,15 @@ def generate_md5(text):
|
|
|
13
14
|
"""Generates the MD5 hash of a string."""
|
|
14
15
|
return hashlib.md5(text.encode('utf-8')).hexdigest()
|
|
15
16
|
|
|
17
|
+
def get_git_user_info():
|
|
18
|
+
"""Recupera o nome e email configurados no git local."""
|
|
19
|
+
try:
|
|
20
|
+
name = subprocess.run(["git", "config", "user.name"], capture_output=True, text=True, check=True).stdout.strip()
|
|
21
|
+
email = subprocess.run(["git", "config", "user.email"], capture_output=True, text=True, check=True).stdout.strip()
|
|
22
|
+
return name, email
|
|
23
|
+
except subprocess.CalledProcessError:
|
|
24
|
+
return "unknown", "unknown"
|
|
25
|
+
|
|
16
26
|
def get_cached_response(action_folder, prompt_text):
|
|
17
27
|
"""Checks if a valid cache exists for the prompt and returns the content."""
|
|
18
28
|
md5_hash = generate_md5(prompt_text)
|
|
@@ -27,7 +37,7 @@ def get_cached_response(action_folder, prompt_text):
|
|
|
27
37
|
return None
|
|
28
38
|
return None
|
|
29
39
|
|
|
30
|
-
def save_cached_response(action_folder, action_type, prompt_text, response_dict):
|
|
40
|
+
def save_cached_response(action_folder, action_type, prompt_text, response_dict, meta_raw=None):
|
|
31
41
|
"""Saves the AI response to the local cache."""
|
|
32
42
|
md5_hash = generate_md5(prompt_text)
|
|
33
43
|
folder_path = get_cache_base_dir() / action_folder
|
|
@@ -37,11 +47,17 @@ def save_cached_response(action_folder, action_type, prompt_text, response_dict)
|
|
|
37
47
|
from src.core import get_current_branch, get_repo_name
|
|
38
48
|
current_branch = get_current_branch()
|
|
39
49
|
repo_name = get_repo_name()
|
|
50
|
+
user_name, user_email = get_git_user_info()
|
|
51
|
+
|
|
52
|
+
if meta_raw is not None:
|
|
53
|
+
response_dict["meta_raw"] = meta_raw
|
|
40
54
|
|
|
41
55
|
cache_data = {
|
|
42
56
|
"md5": md5_hash,
|
|
43
57
|
"repo": repo_name,
|
|
44
58
|
"branch": current_branch,
|
|
59
|
+
"author_name": user_name,
|
|
60
|
+
"author_email": user_email,
|
|
45
61
|
"datetime": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
|
46
62
|
"action_type": action_type,
|
|
47
63
|
"prompt": prompt_text,
|
|
@@ -211,7 +211,35 @@ def get_github_token():
|
|
|
211
211
|
"""Reads and decrypts the GitHub Personal Access Token (PAT)."""
|
|
212
212
|
load_dotenv(ENV_FILE)
|
|
213
213
|
encrypted_token = os.getenv("GITHUB_TOKEN_ENCRYPTED")
|
|
214
|
-
|
|
214
|
+
|
|
215
215
|
if encrypted_token:
|
|
216
216
|
return decrypt_data(encrypted_token)
|
|
217
|
-
return None
|
|
217
|
+
return None
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def validate_github_token(token):
|
|
221
|
+
"""
|
|
222
|
+
Validates a GitHub PAT by making a lightweight API call to /user.
|
|
223
|
+
Returns (is_valid: bool, error_message: str).
|
|
224
|
+
"""
|
|
225
|
+
import requests
|
|
226
|
+
|
|
227
|
+
try:
|
|
228
|
+
headers = {
|
|
229
|
+
"Authorization": f"token {token}",
|
|
230
|
+
"Accept": "application/vnd.github.v3+json"
|
|
231
|
+
}
|
|
232
|
+
response = requests.get("https://api.github.com/user", headers=headers, timeout=10)
|
|
233
|
+
|
|
234
|
+
if response.status_code == 200:
|
|
235
|
+
return True, ""
|
|
236
|
+
elif response.status_code == 401:
|
|
237
|
+
return False, __("Token expired or invalid. Please generate a new one.")
|
|
238
|
+
else:
|
|
239
|
+
return False, __("Unexpected response from GitHub (HTTP {code})", code=response.status_code)
|
|
240
|
+
except requests.exceptions.ConnectionError:
|
|
241
|
+
return False, __("No internet connection. Cannot validate GitHub token.")
|
|
242
|
+
except requests.exceptions.Timeout:
|
|
243
|
+
return False, __("GitHub API timeout. Check your connection and try again.")
|
|
244
|
+
except Exception as e:
|
|
245
|
+
return False, __("Failed to validate token: {error}", error=str(e))
|
|
@@ -16,6 +16,7 @@ from src.config import get_api_key, get_api_model, get_skill_dir, resolve_skill_
|
|
|
16
16
|
from src.ai_providers import call_ai_model
|
|
17
17
|
from src.i18n import __, CURRENT_LANG
|
|
18
18
|
from src.updater import __lang_version__
|
|
19
|
+
# Metrics are imported lazily inside generate_pr_content() to avoid circular imports
|
|
19
20
|
|
|
20
21
|
# Smart Diff filter: files that consume AI tokens without adding semantic value.
|
|
21
22
|
# The pattern list is managed remotely (templates/gitpr.smart-excludes.json) and
|
|
@@ -261,6 +262,8 @@ def generate_pr_content(action_folder, action_type, diff_text, provider="gemini"
|
|
|
261
262
|
click.secho(__("⚠️ No diff found. Make some changes before running the command."), fg="yellow")
|
|
262
263
|
return None
|
|
263
264
|
|
|
265
|
+
t_start = time.perf_counter()
|
|
266
|
+
|
|
264
267
|
# Cache folder configuration
|
|
265
268
|
action_folder_map = {
|
|
266
269
|
"pr": "pr_desc",
|
|
@@ -298,6 +301,15 @@ def generate_pr_content(action_folder, action_type, diff_text, provider="gemini"
|
|
|
298
301
|
cached_data = get_cached_response(action_folder, prompt)
|
|
299
302
|
if cached_data:
|
|
300
303
|
click.secho(__("⚡ Response retrieved from local cache."), fg="green", dim=True)
|
|
304
|
+
from src.metrics import log_command_metric
|
|
305
|
+
duration_ms = int((time.perf_counter() - t_start) * 1000)
|
|
306
|
+
log_command_metric(
|
|
307
|
+
command=action_type,
|
|
308
|
+
status="success",
|
|
309
|
+
provider=provider,
|
|
310
|
+
duration_ms=duration_ms,
|
|
311
|
+
cache_hit=True,
|
|
312
|
+
)
|
|
301
313
|
return cached_data
|
|
302
314
|
|
|
303
315
|
# Key Preparation (Now dynamic per Provider)
|
|
@@ -313,15 +325,27 @@ def generate_pr_content(action_folder, action_type, diff_text, provider="gemini"
|
|
|
313
325
|
click.secho(__("❌ Error: Could not determine model for provider '{provider}'.", provider=provider), fg="red")
|
|
314
326
|
return None
|
|
315
327
|
|
|
316
|
-
# API CALL
|
|
317
328
|
# API CALL
|
|
318
329
|
click.secho(__("🤖 GitPR is analyzing your code using {provider} ({model})...\n", provider=provider.capitalize(), model=api_model), fg="cyan")
|
|
319
330
|
|
|
320
331
|
chunks = split_diff_into_chunks(diff_text, max_tokens=90000)
|
|
321
|
-
|
|
332
|
+
total_meta = {"prompt_tokens": 0, "completion_tokens": 0, "total_tokens": 0, "duration_ms": 0}
|
|
333
|
+
|
|
334
|
+
def _aggregate_meta(new_meta):
|
|
335
|
+
if new_meta:
|
|
336
|
+
total_meta["prompt_tokens"] += new_meta.get("prompt_tokens", 0)
|
|
337
|
+
total_meta["completion_tokens"] += new_meta.get("completion_tokens", 0)
|
|
338
|
+
total_meta["total_tokens"] += new_meta.get("total_tokens", 0)
|
|
339
|
+
total_meta["duration_ms"] += new_meta.get("duration_ms", 0)
|
|
340
|
+
|
|
322
341
|
if len(chunks) == 1:
|
|
323
342
|
result_json = call_ai_model(provider, api_key, api_model, prompt, instrucao_sistema, action=action_folder)
|
|
343
|
+
if result_json:
|
|
344
|
+
_aggregate_meta(result_json.pop("_telemetry_meta", None))
|
|
324
345
|
else:
|
|
346
|
+
from src.metrics import log_local_metric
|
|
347
|
+
log_local_metric(command="map_reduce", status="triggered", map_reduce_triggered=True, chunks_count=len(chunks))
|
|
348
|
+
|
|
325
349
|
click.secho(__("📦 Huge diff detected! Processing in {count} batches (Map-Reduce)...", count=len(chunks)), fg="yellow", bold=True)
|
|
326
350
|
click.secho(f"📚 {__('Understand why:')} {get_doc_url('map-reduce-diff.md')}\n", fg="blue", underline=True)
|
|
327
351
|
resumos_parciais = []
|
|
@@ -332,9 +356,11 @@ def generate_pr_content(action_folder, action_type, diff_text, provider="gemini"
|
|
|
332
356
|
prompt_parcial = __("Generate ONLY a JSON object in the format {json_format} containing a technical summary of what was changed in this part ({idx}) of the diff:\n", json_format='{"resumo": "..."}', idx=i) + chunk
|
|
333
357
|
|
|
334
358
|
resposta_parcial = call_ai_model(provider, api_key, api_model, prompt_parcial, instrucao_sistema, quiet=True, action=f"{action_folder}_chunk_{i}")
|
|
335
|
-
|
|
336
|
-
if resposta_parcial
|
|
337
|
-
|
|
359
|
+
|
|
360
|
+
if resposta_parcial:
|
|
361
|
+
_aggregate_meta(resposta_parcial.pop("_telemetry_meta", None))
|
|
362
|
+
if "resumo" in resposta_parcial:
|
|
363
|
+
resumos_parciais.append(f"### Batch {i}\n{resposta_parcial['resumo']}")
|
|
338
364
|
|
|
339
365
|
time.sleep(1)
|
|
340
366
|
|
|
@@ -353,12 +379,29 @@ def generate_pr_content(action_folder, action_type, diff_text, provider="gemini"
|
|
|
353
379
|
prompt = __("Unify these technical summaries and generate ONLY a JSON object in the format {json_format} describing the Pull Request:\n", json_format='{"commit_message": "...", "pr_description": "..."}') + diff_unificado
|
|
354
380
|
|
|
355
381
|
result_json = call_ai_model(provider, api_key, api_model, prompt, instrucao_sistema, action=action_folder)
|
|
382
|
+
if result_json:
|
|
383
|
+
_aggregate_meta(result_json.pop("_telemetry_meta", None))
|
|
356
384
|
|
|
357
385
|
# SAVE TO CACHE AND RETURN
|
|
358
386
|
if result_json:
|
|
359
|
-
save_cached_response(action_folder, action_type, prompt, result_json)
|
|
387
|
+
save_cached_response(action_folder, action_type, prompt, result_json, meta_raw=total_meta)
|
|
388
|
+
# Fire-and-forget metric for successful AI-powered command
|
|
389
|
+
from src.metrics import log_command_metric
|
|
390
|
+
duration_ms = int((time.perf_counter() - t_start) * 1000)
|
|
391
|
+
log_command_metric(
|
|
392
|
+
command=action_type,
|
|
393
|
+
status="success",
|
|
394
|
+
provider=provider,
|
|
395
|
+
tokens_estimated=total_meta.get("total_tokens", 0),
|
|
396
|
+
duration_ms=duration_ms,
|
|
397
|
+
map_reduce_triggered=(len(chunks) > 1),
|
|
398
|
+
)
|
|
360
399
|
return result_json
|
|
361
|
-
|
|
400
|
+
|
|
401
|
+
# Command failed (AI returned None)
|
|
402
|
+
from src.metrics import log_command_metric
|
|
403
|
+
duration_ms = int((time.perf_counter() - t_start) * 1000)
|
|
404
|
+
log_command_metric(command=action_type, status="error", provider=provider, duration_ms=duration_ms)
|
|
362
405
|
return None
|
|
363
406
|
|
|
364
407
|
|
|
@@ -487,7 +530,10 @@ def install_git_hooks():
|
|
|
487
530
|
# Mapping: Hook Name in Git -> Template Name on your GitHub
|
|
488
531
|
hooks_to_install = {
|
|
489
532
|
"pre-commit": "pre-commit-template.sh",
|
|
490
|
-
"prepare-commit-msg": "prepare-commit-msg-template.sh"
|
|
533
|
+
"prepare-commit-msg": "prepare-commit-msg-template.sh",
|
|
534
|
+
"post-checkout": "post-checkout-template.sh",
|
|
535
|
+
"pre-push": "pre-push-template.sh",
|
|
536
|
+
"post-merge": "post-merge-template.sh",
|
|
491
537
|
}
|
|
492
538
|
|
|
493
539
|
base_url = "https://raw.githubusercontent.com/natanfiuza/gitpr/main/scripts/"
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import subprocess
|
|
2
2
|
import re
|
|
3
3
|
import os
|
|
4
|
+
import time
|
|
4
5
|
import click
|
|
5
6
|
from src.ai_providers import call_ai_model
|
|
6
7
|
from src.cache import get_cached_response, save_cached_response
|
|
@@ -32,14 +33,20 @@ def get_github_repo_info():
|
|
|
32
33
|
|
|
33
34
|
def generate_issue_content(context_text, context_type="diff"):
|
|
34
35
|
"""Sends the context (diff, blame, or history) to the AI and returns an issue dictionary."""
|
|
36
|
+
from src.metrics import log_command_metric
|
|
37
|
+
|
|
35
38
|
if not context_text or not str(context_text).strip():
|
|
36
39
|
return None
|
|
37
40
|
|
|
41
|
+
t_start = time.perf_counter()
|
|
42
|
+
|
|
38
43
|
provider = get_ai_provider()
|
|
39
44
|
api_key = get_api_key(provider)
|
|
40
45
|
|
|
41
46
|
if not api_key:
|
|
42
47
|
click.secho(__("❌ Error: API Key not found."), fg="red")
|
|
48
|
+
duration_ms = int((time.perf_counter() - t_start) * 1000)
|
|
49
|
+
log_command_metric(command="issue", status="error", provider=provider, duration_ms=duration_ms)
|
|
43
50
|
return None
|
|
44
51
|
|
|
45
52
|
# Use the advanced model to ensure Issue structure quality
|
|
@@ -69,11 +76,13 @@ def generate_issue_content(context_text, context_type="diff"):
|
|
|
69
76
|
__("Generate the requested JSON object following the system instructions to {target_action}\n\n", target_action=target_action) +
|
|
70
77
|
f"{data_label}\n{context_text}"
|
|
71
78
|
)
|
|
72
|
-
|
|
79
|
+
|
|
73
80
|
# Try to retrieve from Cache
|
|
74
81
|
cached_data = get_cached_response("issue", prompt)
|
|
75
82
|
if cached_data:
|
|
76
83
|
click.secho(__("⚡ Issue response retrieved from local cache."), fg="green", dim=True)
|
|
84
|
+
duration_ms = int((time.perf_counter() - t_start) * 1000)
|
|
85
|
+
log_command_metric(command="issue", status="success", provider=provider, duration_ms=duration_ms, cache_hit=True)
|
|
77
86
|
return cached_data
|
|
78
87
|
|
|
79
88
|
click.secho(__("🤖 Structuring Issue using {provider} ({api_model})...", provider=provider.capitalize(), api_model=api_model), fg="cyan", dim=True)
|
|
@@ -81,8 +90,19 @@ def generate_issue_content(context_text, context_type="diff"):
|
|
|
81
90
|
result_json = call_ai_model(provider, api_key, api_model, prompt, sys_inst, action="issue")
|
|
82
91
|
|
|
83
92
|
if result_json and "titulo" in result_json and "corpo" in result_json:
|
|
84
|
-
#
|
|
85
|
-
|
|
93
|
+
# Extract telemetry metadata and save to cache with real token counts
|
|
94
|
+
meta = result_json.pop("_telemetry_meta", None)
|
|
95
|
+
save_cached_response("issue", "issue", prompt, result_json, meta_raw=meta)
|
|
96
|
+
duration_ms = int((time.perf_counter() - t_start) * 1000)
|
|
97
|
+
log_command_metric(
|
|
98
|
+
command="issue",
|
|
99
|
+
status="success",
|
|
100
|
+
provider=provider,
|
|
101
|
+
tokens_estimated=(meta or {}).get("total_tokens", 0),
|
|
102
|
+
duration_ms=duration_ms,
|
|
103
|
+
)
|
|
86
104
|
return result_json
|
|
87
|
-
|
|
105
|
+
|
|
106
|
+
duration_ms = int((time.perf_counter() - t_start) * 1000)
|
|
107
|
+
log_command_metric(command="issue", status="error", provider=provider, duration_ms=duration_ms)
|
|
88
108
|
return {"titulo": __("Error generating title"), "corpo": __("Could not generate issue body by AI.")}
|