gitpr-cli 0.0.17__tar.gz → 0.0.19__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.17 → gitpr_cli-0.0.19}/PKG-INFO +3 -1
- {gitpr_cli-0.0.17 → gitpr_cli-0.0.19}/gitpr_cli.egg-info/PKG-INFO +3 -1
- {gitpr_cli-0.0.17 → gitpr_cli-0.0.19}/gitpr_cli.egg-info/SOURCES.txt +3 -0
- {gitpr_cli-0.0.17 → gitpr_cli-0.0.19}/gitpr_cli.egg-info/requires.txt +2 -0
- {gitpr_cli-0.0.17 → gitpr_cli-0.0.19}/pyproject.toml +7 -4
- {gitpr_cli-0.0.17 → gitpr_cli-0.0.19}/src/cache.py +14 -12
- {gitpr_cli-0.0.17 → gitpr_cli-0.0.19}/src/core.py +33 -11
- gitpr_cli-0.0.19/src/ui/__init__.py +0 -0
- gitpr_cli-0.0.19/src/ui/help_screen.py +39 -0
- gitpr_cli-0.0.19/src/ui/issue_app.py +116 -0
- {gitpr_cli-0.0.17 → gitpr_cli-0.0.19}/src/updater.py +1 -1
- {gitpr_cli-0.0.17 → gitpr_cli-0.0.19}/LICENSE +0 -0
- {gitpr_cli-0.0.17 → gitpr_cli-0.0.19}/README.md +0 -0
- {gitpr_cli-0.0.17 → gitpr_cli-0.0.19}/gitpr_cli.egg-info/dependency_links.txt +0 -0
- {gitpr_cli-0.0.17 → gitpr_cli-0.0.19}/gitpr_cli.egg-info/entry_points.txt +0 -0
- {gitpr_cli-0.0.17 → gitpr_cli-0.0.19}/gitpr_cli.egg-info/top_level.txt +0 -0
- {gitpr_cli-0.0.17 → gitpr_cli-0.0.19}/setup.cfg +0 -0
- {gitpr_cli-0.0.17 → gitpr_cli-0.0.19}/src/__init__.py +0 -0
- {gitpr_cli-0.0.17 → gitpr_cli-0.0.19}/src/ai_providers.py +0 -0
- {gitpr_cli-0.0.17 → gitpr_cli-0.0.19}/src/blame_engine.py +0 -0
- {gitpr_cli-0.0.17 → gitpr_cli-0.0.19}/src/config.py +0 -0
- {gitpr_cli-0.0.17 → gitpr_cli-0.0.19}/src/issue_engine.py +0 -0
- {gitpr_cli-0.0.17 → gitpr_cli-0.0.19}/src/linter_engine.py +0 -0
- {gitpr_cli-0.0.17 → gitpr_cli-0.0.19}/src/main.py +0 -0
- {gitpr_cli-0.0.17 → gitpr_cli-0.0.19}/src/security.py +0 -0
- {gitpr_cli-0.0.17 → gitpr_cli-0.0.19}/src/tui_issue.py +0 -0
- {gitpr_cli-0.0.17 → gitpr_cli-0.0.19}/tests/test_core.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: gitpr-cli
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.19
|
|
4
4
|
Summary: Automação de PRs, Commits e Code Review com IA (Gemini e DeepSeek)
|
|
5
5
|
Author-email: Natan Fiuza <contato@natanfiuza.dev.br>
|
|
6
6
|
Requires-Python: >=3.10
|
|
@@ -12,6 +12,8 @@ Requires-Dist: openai
|
|
|
12
12
|
Requires-Dist: python-dotenv
|
|
13
13
|
Requires-Dist: cryptography
|
|
14
14
|
Requires-Dist: pyyaml
|
|
15
|
+
Requires-Dist: textual
|
|
16
|
+
Requires-Dist: requests
|
|
15
17
|
Dynamic: license-file
|
|
16
18
|
|
|
17
19
|
# **GitPR CLI 🚀**
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: gitpr-cli
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.19
|
|
4
4
|
Summary: Automação de PRs, Commits e Code Review com IA (Gemini e DeepSeek)
|
|
5
5
|
Author-email: Natan Fiuza <contato@natanfiuza.dev.br>
|
|
6
6
|
Requires-Python: >=3.10
|
|
@@ -12,6 +12,8 @@ Requires-Dist: openai
|
|
|
12
12
|
Requires-Dist: python-dotenv
|
|
13
13
|
Requires-Dist: cryptography
|
|
14
14
|
Requires-Dist: pyyaml
|
|
15
|
+
Requires-Dist: textual
|
|
16
|
+
Requires-Dist: requests
|
|
15
17
|
Dynamic: license-file
|
|
16
18
|
|
|
17
19
|
# **GitPR CLI 🚀**
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "gitpr-cli"
|
|
7
|
-
version = "0.0.
|
|
7
|
+
version = "0.0.19"
|
|
8
8
|
description = "Automação de PRs, Commits e Code Review com IA (Gemini e DeepSeek)"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -17,11 +17,14 @@ dependencies = [
|
|
|
17
17
|
"openai",
|
|
18
18
|
"python-dotenv",
|
|
19
19
|
"cryptography",
|
|
20
|
-
"pyyaml"
|
|
20
|
+
"pyyaml",
|
|
21
|
+
"textual",
|
|
22
|
+
"requests"
|
|
21
23
|
]
|
|
22
24
|
|
|
23
25
|
[project.scripts]
|
|
24
26
|
gitpr = "src.main:cli"
|
|
25
27
|
|
|
26
|
-
[tool.setuptools]
|
|
27
|
-
|
|
28
|
+
[tool.setuptools.packages.find]
|
|
29
|
+
where = ["."]
|
|
30
|
+
include = ["src", "src.*"]
|
|
@@ -33,11 +33,13 @@ def save_cached_response(action_folder, action_type, prompt_text, response_dict)
|
|
|
33
33
|
folder_path.mkdir(parents=True, exist_ok=True)
|
|
34
34
|
|
|
35
35
|
cache_file = folder_path / f"{md5_hash}.json"
|
|
36
|
-
from src.core import get_current_branch
|
|
36
|
+
from src.core import get_current_branch, get_repo_name
|
|
37
37
|
current_branch = get_current_branch()
|
|
38
|
-
|
|
38
|
+
repo_name = get_repo_name()
|
|
39
|
+
|
|
39
40
|
cache_data = {
|
|
40
41
|
"md5": md5_hash,
|
|
42
|
+
"repo": repo_name,
|
|
41
43
|
"branch": current_branch,
|
|
42
44
|
"datetime": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
|
43
45
|
"action_type": action_type,
|
|
@@ -51,21 +53,21 @@ def save_cached_response(action_folder, action_type, prompt_text, response_dict)
|
|
|
51
53
|
except IOError:
|
|
52
54
|
pass # Falha silenciosa no cache para não travar a ferramenta
|
|
53
55
|
|
|
54
|
-
def get_cached_pr_descriptions(branch_name):
|
|
55
|
-
"""Busca no cache todos os PRs gerados historicamente para
|
|
56
|
+
def get_cached_pr_descriptions(repo_name, branch_name):
|
|
57
|
+
"""Busca no cache todos os PRs gerados historicamente para este repositorio e branch."""
|
|
56
58
|
pr_cache_folder = get_cache_base_dir() / "pr_desc"
|
|
57
59
|
history_texts = []
|
|
58
|
-
|
|
60
|
+
|
|
59
61
|
if not pr_cache_folder.exists():
|
|
60
62
|
return ""
|
|
61
|
-
|
|
63
|
+
|
|
62
64
|
for cache_file in pr_cache_folder.glob("*.json"):
|
|
63
65
|
try:
|
|
64
66
|
with open(cache_file, "r", encoding="utf-8") as f:
|
|
65
67
|
data = json.load(f)
|
|
66
|
-
|
|
67
|
-
#
|
|
68
|
-
if data.get("branch") == branch_name:
|
|
68
|
+
|
|
69
|
+
# Filtra por repositorio E branch (evita misturar projetos diferentes)
|
|
70
|
+
if data.get("repo") == repo_name and data.get("branch") == branch_name:
|
|
69
71
|
response_dict = data.get("response", {})
|
|
70
72
|
pr_desc = response_dict.get("pr_description")
|
|
71
73
|
if pr_desc:
|
|
@@ -73,10 +75,10 @@ def get_cached_pr_descriptions(branch_name):
|
|
|
73
75
|
history_texts.append(f"[{date_str}]\n{pr_desc}\n")
|
|
74
76
|
except (json.JSONDecodeError, IOError):
|
|
75
77
|
continue
|
|
76
|
-
|
|
78
|
+
|
|
77
79
|
if history_texts:
|
|
78
|
-
# Ordena cronologicamente (usando a data
|
|
80
|
+
# Ordena cronologicamente (usando a data extraida no colchete)
|
|
79
81
|
history_texts.sort()
|
|
80
82
|
return "=== HISTÓRICO DE PRs DA IA ===\n" + "\n".join(history_texts)
|
|
81
|
-
|
|
83
|
+
|
|
82
84
|
return ""
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import os
|
|
2
|
+
import re
|
|
2
3
|
import json
|
|
3
4
|
import stat
|
|
4
5
|
import click
|
|
@@ -56,9 +57,9 @@ def get_current_branch():
|
|
|
56
57
|
"""Retorna o nome da branch atual."""
|
|
57
58
|
try:
|
|
58
59
|
result = subprocess.run(
|
|
59
|
-
["git", "rev-parse", "--abbrev-ref", "HEAD"],
|
|
60
|
-
capture_output=True,
|
|
61
|
-
text=True,
|
|
60
|
+
["git", "rev-parse", "--abbrev-ref", "HEAD"],
|
|
61
|
+
capture_output=True,
|
|
62
|
+
text=True,
|
|
62
63
|
encoding="utf-8",
|
|
63
64
|
check=True
|
|
64
65
|
)
|
|
@@ -67,6 +68,26 @@ def get_current_branch():
|
|
|
67
68
|
return "main" # Fallback
|
|
68
69
|
|
|
69
70
|
|
|
71
|
+
def get_repo_name():
|
|
72
|
+
"""Extrai o nome owner/repo do git remote origin."""
|
|
73
|
+
try:
|
|
74
|
+
result = subprocess.run(
|
|
75
|
+
["git", "remote", "-v"],
|
|
76
|
+
capture_output=True,
|
|
77
|
+
text=True,
|
|
78
|
+
encoding="utf-8",
|
|
79
|
+
check=True
|
|
80
|
+
)
|
|
81
|
+
match = re.search(r'github\.com[:/](.+?)/(.+?)(\.git)?\s+\(push\)', result.stdout)
|
|
82
|
+
if match:
|
|
83
|
+
owner = match.group(1)
|
|
84
|
+
repo = match.group(2).replace('.git', '')
|
|
85
|
+
return f"{owner}/{repo}"
|
|
86
|
+
return "unknown/repo"
|
|
87
|
+
except subprocess.CalledProcessError:
|
|
88
|
+
return "unknown/repo"
|
|
89
|
+
|
|
90
|
+
|
|
70
91
|
def get_skill_context(action_type="pr"):
|
|
71
92
|
"""Lê o arquivo de contexto correto baseado na ação (PR/Commit ou Review)."""
|
|
72
93
|
|
|
@@ -316,14 +337,15 @@ def install_git_hooks():
|
|
|
316
337
|
return success_count == len(hooks_to_install)
|
|
317
338
|
|
|
318
339
|
def get_branch_history_text():
|
|
319
|
-
"""Compila o Git Log e o Cache de PRs da branch atual para gerar o contexto
|
|
340
|
+
"""Compila o Git Log e o Cache de PRs da branch atual para gerar o contexto epico."""
|
|
320
341
|
branch = get_current_branch()
|
|
321
342
|
base_branch = get_base_branch()
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
343
|
+
repo_name = get_repo_name()
|
|
344
|
+
|
|
345
|
+
click.secho(f"🔄 Compilando historico do repositorio '{repo_name}', branch '{branch}' contra '{base_branch}'...", fg="cyan")
|
|
346
|
+
|
|
347
|
+
hybrid_context = f"Repositorio: {repo_name}\nResumo Historico da Branch: {branch}\n\n"
|
|
348
|
+
|
|
327
349
|
# Pega os Commits reais do Git
|
|
328
350
|
try:
|
|
329
351
|
# Pega a linha do tempo desde o merge base
|
|
@@ -349,8 +371,8 @@ def get_branch_history_text():
|
|
|
349
371
|
except subprocess.CalledProcessError as e:
|
|
350
372
|
click.secho(f"⚠️ Aviso: Não foi possível obter o Git Log: {e.stderr}", fg="yellow")
|
|
351
373
|
|
|
352
|
-
# Pega a
|
|
353
|
-
cached_prs = get_cached_pr_descriptions(branch)
|
|
374
|
+
# Pega a memoria historica da IA (Cache de PRs antigos desse repo + branch)
|
|
375
|
+
cached_prs = get_cached_pr_descriptions(repo_name, branch)
|
|
354
376
|
if cached_prs:
|
|
355
377
|
hybrid_context += f"{cached_prs}\n"
|
|
356
378
|
else:
|
|
File without changes
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
from textual.app import ComposeResult
|
|
2
|
+
from textual.screen import ModalScreen
|
|
3
|
+
from textual.widgets import Button, Static
|
|
4
|
+
from textual.containers import Vertical
|
|
5
|
+
|
|
6
|
+
class HelpScreen(ModalScreen):
|
|
7
|
+
"""Modal de ajuda com as instruções de uso da interface."""
|
|
8
|
+
|
|
9
|
+
CSS = """
|
|
10
|
+
HelpScreen { align: center middle; }
|
|
11
|
+
#help_dialog {
|
|
12
|
+
width: 90; height: auto; padding: 1 2; background: $surface; border: thick $background 80%;
|
|
13
|
+
align-horizontal: center;
|
|
14
|
+
}
|
|
15
|
+
.help_title { text-align: center; text-style: bold; margin-bottom: 1; }
|
|
16
|
+
.help_text { margin-bottom: 1; }
|
|
17
|
+
Button {
|
|
18
|
+
width: 20%;
|
|
19
|
+
margin-top: 1;
|
|
20
|
+
}
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
def compose(self) -> ComposeResult:
|
|
24
|
+
with Vertical(id="help_dialog"):
|
|
25
|
+
yield Static("💡 Ajuda do GitPR Issue", classes="help_title")
|
|
26
|
+
yield Static(
|
|
27
|
+
"• F4 (Ajuda): Exibe este modal de instruções.\n"
|
|
28
|
+
"• F2 (Salvar Local): Gera um arquivo Markdown (.md) com a issue.\n"
|
|
29
|
+
"• F3 (Criar no GitHub): Cria a issue remotamente via API.\n"
|
|
30
|
+
"• Esc (Sair): Fecha o aplicativo sem salvar.\n\n"
|
|
31
|
+
"📚 Leia o guia completo de utilização da interface TUI:\n"
|
|
32
|
+
"https://github.com/natanfiuza/gitpr/blob/main/docs/issue-tui-help.md",
|
|
33
|
+
classes="help_text"
|
|
34
|
+
)
|
|
35
|
+
yield Button("Entendi", variant="primary", id="close_help")
|
|
36
|
+
|
|
37
|
+
def on_button_pressed(self, event: Button.Pressed) -> None:
|
|
38
|
+
if event.button.id == "close_help":
|
|
39
|
+
self.app.pop_screen()
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import requests
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from textual.app import App, ComposeResult
|
|
5
|
+
from textual.widgets import Header, Footer, Input, TextArea, Label
|
|
6
|
+
from textual.containers import Vertical
|
|
7
|
+
from textual.binding import Binding
|
|
8
|
+
|
|
9
|
+
from src.core import get_current_branch
|
|
10
|
+
from src.ui.help_screen import HelpScreen
|
|
11
|
+
|
|
12
|
+
class IssueApp(App):
|
|
13
|
+
"""Interface de Terminal para edição e submissão da Issue."""
|
|
14
|
+
|
|
15
|
+
TITLE = "GitPR - Gerador de Issues"
|
|
16
|
+
ENABLE_COMMAND_PALETTE = False
|
|
17
|
+
|
|
18
|
+
CSS = """
|
|
19
|
+
Input { margin-bottom: 1; }
|
|
20
|
+
TextArea { height: 1fr; }
|
|
21
|
+
Label { margin-top: 1; text-style: bold; color: $accent; }
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
BINDINGS = [
|
|
25
|
+
Binding("f4", "show_help", "Ajuda"),
|
|
26
|
+
Binding("f2", "save_local", "Salvar Local"),
|
|
27
|
+
Binding("f3", "create_issue", "Criar no GitHub"),
|
|
28
|
+
Binding("escape", "quit", "Sair")
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
def __init__(self, issue_data, repo_info, github_token, **kwargs):
|
|
32
|
+
super().__init__(**kwargs)
|
|
33
|
+
self.issue_data = issue_data
|
|
34
|
+
self.repo_info = repo_info
|
|
35
|
+
self.github_token = github_token
|
|
36
|
+
self.final_action = None
|
|
37
|
+
self.final_message = ""
|
|
38
|
+
|
|
39
|
+
branch = get_current_branch()
|
|
40
|
+
repo_display = self.repo_info if self.repo_info else "Repositório Local"
|
|
41
|
+
self.sub_title = f"{repo_display} | Branch: {branch}"
|
|
42
|
+
|
|
43
|
+
def compose(self) -> ComposeResult:
|
|
44
|
+
"""Monta o layout da interface."""
|
|
45
|
+
yield Header(show_clock=True)
|
|
46
|
+
with Vertical():
|
|
47
|
+
yield Label("📌 Título da Issue")
|
|
48
|
+
yield Input(value=self.issue_data.get("titulo", ""), id="issue_title")
|
|
49
|
+
|
|
50
|
+
yield Label("📝 Corpo da Issue")
|
|
51
|
+
yield TextArea(text=self.issue_data.get("corpo", ""), id="issue_body")
|
|
52
|
+
yield Footer()
|
|
53
|
+
|
|
54
|
+
def action_show_help(self):
|
|
55
|
+
"""Ação do botão F1: Exibe o modal de ajuda."""
|
|
56
|
+
self.push_screen(HelpScreen())
|
|
57
|
+
|
|
58
|
+
def action_save_local(self):
|
|
59
|
+
"""Ação do botão F2: Salva o conteúdo em um arquivo markdown local."""
|
|
60
|
+
title_input = self.query_one("#issue_title", Input)
|
|
61
|
+
body_input = self.query_one("#issue_body", TextArea)
|
|
62
|
+
|
|
63
|
+
branch_name = get_current_branch().replace("/", "-").replace("\\", "-")
|
|
64
|
+
current_time = datetime.now().strftime("%Y%m%d%H%M%S")
|
|
65
|
+
|
|
66
|
+
pattern = os.getenv("OUTPUT_FILE_NAME_ISSUE", "{branch}_{datetime}_ISSUE.md")
|
|
67
|
+
output_filename = pattern.format(branch=branch_name, datetime=current_time)
|
|
68
|
+
|
|
69
|
+
md_content = f"# {title_input.value}\n\n{body_input.text}"
|
|
70
|
+
|
|
71
|
+
try:
|
|
72
|
+
with open(output_filename, "w", encoding="utf-8") as f:
|
|
73
|
+
f.write(md_content)
|
|
74
|
+
self.final_message = f"✅ Issue salva localmente: {output_filename}"
|
|
75
|
+
self.final_action = "saved"
|
|
76
|
+
except Exception as e:
|
|
77
|
+
self.final_message = f"❌ Erro ao salvar arquivo: {e}"
|
|
78
|
+
self.final_action = "error"
|
|
79
|
+
|
|
80
|
+
self.exit()
|
|
81
|
+
|
|
82
|
+
def action_create_issue(self):
|
|
83
|
+
"""Ação do botão F3: Envia a issue via API REST para o GitHub."""
|
|
84
|
+
title_input = self.query_one("#issue_title", Input)
|
|
85
|
+
body_input = self.query_one("#issue_body", TextArea)
|
|
86
|
+
|
|
87
|
+
if not self.repo_info:
|
|
88
|
+
self.final_message = "❌ Repositório remoto não identificado para criar a issue via API."
|
|
89
|
+
self.final_action = "error"
|
|
90
|
+
self.exit()
|
|
91
|
+
return
|
|
92
|
+
|
|
93
|
+
api_url = f"https://api.github.com/repos/{self.repo_info}/issues"
|
|
94
|
+
headers = {
|
|
95
|
+
"Authorization": f"token {self.github_token}",
|
|
96
|
+
"Accept": "application/vnd.github.v3+json"
|
|
97
|
+
}
|
|
98
|
+
payload = {
|
|
99
|
+
"title": title_input.value,
|
|
100
|
+
"body": body_input.text
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
try:
|
|
104
|
+
response = requests.post(api_url, json=payload, headers=headers)
|
|
105
|
+
if response.status_code == 201:
|
|
106
|
+
issue_url = response.json().get("html_url")
|
|
107
|
+
self.final_message = f"✅ Issue criada com sucesso no GitHub:\n👉 {issue_url}"
|
|
108
|
+
self.final_action = "created"
|
|
109
|
+
else:
|
|
110
|
+
self.final_message = f"❌ Erro na API do GitHub ({response.status_code}): {response.text}"
|
|
111
|
+
self.final_action = "error"
|
|
112
|
+
except Exception as e:
|
|
113
|
+
self.final_message = f"❌ Falha na conexão com o GitHub: {e}"
|
|
114
|
+
self.final_action = "error"
|
|
115
|
+
|
|
116
|
+
self.exit()
|
|
@@ -7,7 +7,7 @@ import click
|
|
|
7
7
|
from datetime import datetime
|
|
8
8
|
|
|
9
9
|
# Versão atual do seu executável local (Atualize isso a cada novo build!)
|
|
10
|
-
__version__ = "0.0.
|
|
10
|
+
__version__ = "0.0.19"
|
|
11
11
|
GITHUB_API_URL = "https://api.github.com/repos/natanfiuza/gitpr/releases/latest"
|
|
12
12
|
PYPI_API_URL = "https://pypi.org/pypi/gitpr-cli/json"
|
|
13
13
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|