gitpr-cli 0.0.12__tar.gz → 0.0.14__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.12 → gitpr_cli-0.0.14}/PKG-INFO +6 -1
- {gitpr_cli-0.0.12 → gitpr_cli-0.0.14}/README.md +5 -0
- {gitpr_cli-0.0.12 → gitpr_cli-0.0.14}/gitpr_cli.egg-info/PKG-INFO +6 -1
- {gitpr_cli-0.0.12 → gitpr_cli-0.0.14}/gitpr_cli.egg-info/SOURCES.txt +1 -0
- {gitpr_cli-0.0.12 → gitpr_cli-0.0.14}/pyproject.toml +1 -1
- gitpr_cli-0.0.14/src/blame_engine.py +216 -0
- {gitpr_cli-0.0.12 → gitpr_cli-0.0.14}/src/config.py +2 -1
- {gitpr_cli-0.0.12 → gitpr_cli-0.0.14}/src/core.py +1 -0
- {gitpr_cli-0.0.12 → gitpr_cli-0.0.14}/src/main.py +48 -12
- gitpr_cli-0.0.14/src/updater.py +167 -0
- gitpr_cli-0.0.12/src/updater.py +0 -90
- {gitpr_cli-0.0.12 → gitpr_cli-0.0.14}/LICENSE +0 -0
- {gitpr_cli-0.0.12 → gitpr_cli-0.0.14}/gitpr_cli.egg-info/dependency_links.txt +0 -0
- {gitpr_cli-0.0.12 → gitpr_cli-0.0.14}/gitpr_cli.egg-info/entry_points.txt +0 -0
- {gitpr_cli-0.0.12 → gitpr_cli-0.0.14}/gitpr_cli.egg-info/requires.txt +0 -0
- {gitpr_cli-0.0.12 → gitpr_cli-0.0.14}/gitpr_cli.egg-info/top_level.txt +0 -0
- {gitpr_cli-0.0.12 → gitpr_cli-0.0.14}/setup.cfg +0 -0
- {gitpr_cli-0.0.12 → gitpr_cli-0.0.14}/src/__init__.py +0 -0
- {gitpr_cli-0.0.12 → gitpr_cli-0.0.14}/src/ai_providers.py +0 -0
- {gitpr_cli-0.0.12 → gitpr_cli-0.0.14}/src/cache.py +0 -0
- {gitpr_cli-0.0.12 → gitpr_cli-0.0.14}/src/linter_engine.py +0 -0
- {gitpr_cli-0.0.12 → gitpr_cli-0.0.14}/src/security.py +0 -0
- {gitpr_cli-0.0.12 → gitpr_cli-0.0.14}/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.14
|
|
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
|
|
@@ -189,7 +189,12 @@ Nunca mais se preocupe em baixar novas versões manualmente. O GitPR possui um G
|
|
|
189
189
|
* Em cada execução, ele verifica silenciosamente se há um novo release oficial na API do GitHub.
|
|
190
190
|
* Você pode forçar a busca e instalação rodando `gitpr --update` ou `gitpr -u`.
|
|
191
191
|
* A ferramenta utiliza a técnica de *Hot-Swap*, baixando o novo `.exe` e substituindo a versão antiga de forma transparente.
|
|
192
|
+
## Publicar no PyPi
|
|
192
193
|
|
|
194
|
+
```bash
|
|
195
|
+
pipenv run python -m build
|
|
196
|
+
pipenv run twine upload dist/*
|
|
197
|
+
```
|
|
193
198
|
## **🤝 Como Contribuir**
|
|
194
199
|
|
|
195
200
|
Contribuições são muito bem-vindas! Para contribuir:
|
|
@@ -173,7 +173,12 @@ Nunca mais se preocupe em baixar novas versões manualmente. O GitPR possui um G
|
|
|
173
173
|
* Em cada execução, ele verifica silenciosamente se há um novo release oficial na API do GitHub.
|
|
174
174
|
* Você pode forçar a busca e instalação rodando `gitpr --update` ou `gitpr -u`.
|
|
175
175
|
* A ferramenta utiliza a técnica de *Hot-Swap*, baixando o novo `.exe` e substituindo a versão antiga de forma transparente.
|
|
176
|
+
## Publicar no PyPi
|
|
176
177
|
|
|
178
|
+
```bash
|
|
179
|
+
pipenv run python -m build
|
|
180
|
+
pipenv run twine upload dist/*
|
|
181
|
+
```
|
|
177
182
|
## **🤝 Como Contribuir**
|
|
178
183
|
|
|
179
184
|
Contribuições são muito bem-vindas! Para contribuir:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: gitpr-cli
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.14
|
|
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
|
|
@@ -189,7 +189,12 @@ Nunca mais se preocupe em baixar novas versões manualmente. O GitPR possui um G
|
|
|
189
189
|
* Em cada execução, ele verifica silenciosamente se há um novo release oficial na API do GitHub.
|
|
190
190
|
* Você pode forçar a busca e instalação rodando `gitpr --update` ou `gitpr -u`.
|
|
191
191
|
* A ferramenta utiliza a técnica de *Hot-Swap*, baixando o novo `.exe` e substituindo a versão antiga de forma transparente.
|
|
192
|
+
## Publicar no PyPi
|
|
192
193
|
|
|
194
|
+
```bash
|
|
195
|
+
pipenv run python -m build
|
|
196
|
+
pipenv run twine upload dist/*
|
|
197
|
+
```
|
|
193
198
|
## **🤝 Como Contribuir**
|
|
194
199
|
|
|
195
200
|
Contribuições são muito bem-vindas! Para contribuir:
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import subprocess
|
|
2
|
+
import click
|
|
3
|
+
import re
|
|
4
|
+
import os
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
from src.core import get_current_branch
|
|
7
|
+
from src.config import get_api_key, get_api_model, get_ai_provider
|
|
8
|
+
from src.ai_providers import call_ai_model
|
|
9
|
+
|
|
10
|
+
def execute_git_blame(file_path, start_line, end_line, commit_hash=None):
|
|
11
|
+
"""Executa o git blame e retorna uma lista de hashes únicos."""
|
|
12
|
+
cmd = ["git", "blame", f"-L", f"{start_line},{end_line}"]
|
|
13
|
+
if commit_hash:
|
|
14
|
+
cmd.append(commit_hash)
|
|
15
|
+
cmd.extend(["--", file_path])
|
|
16
|
+
|
|
17
|
+
try:
|
|
18
|
+
result = subprocess.run(
|
|
19
|
+
cmd, capture_output=True, text=True, encoding="utf-8", errors="replace", check=True
|
|
20
|
+
)
|
|
21
|
+
hashes = set()
|
|
22
|
+
for line in result.stdout.strip().split('\n'):
|
|
23
|
+
if line:
|
|
24
|
+
match = re.match(r'^([a-fA-F0-9]+)\s', line)
|
|
25
|
+
if match:
|
|
26
|
+
commit = match.group(1)
|
|
27
|
+
if not commit.startswith('000000'):
|
|
28
|
+
hashes.add(commit)
|
|
29
|
+
return list(hashes)
|
|
30
|
+
except subprocess.CalledProcessError as e:
|
|
31
|
+
# Se falhar (ex: arquivo não existia naquele commit antigo), silenciamos e retornamos vazio
|
|
32
|
+
return []
|
|
33
|
+
|
|
34
|
+
def execute_git_show(commit_hash, file_path):
|
|
35
|
+
"""Executa o git show para pegar o diff exato."""
|
|
36
|
+
cmd = ["git", "show", commit_hash, "--", file_path]
|
|
37
|
+
try:
|
|
38
|
+
result = subprocess.run(
|
|
39
|
+
cmd, capture_output=True, text=True, encoding="utf-8", errors="replace", check=True
|
|
40
|
+
)
|
|
41
|
+
return result.stdout
|
|
42
|
+
except subprocess.CalledProcessError:
|
|
43
|
+
return None
|
|
44
|
+
|
|
45
|
+
def get_commit_info(commit_hash):
|
|
46
|
+
"""Busca autor, data e mensagem do commit."""
|
|
47
|
+
cmd = ["git", "show", "-s", "--format=%an|%ad|%s", "--date=short", commit_hash]
|
|
48
|
+
try:
|
|
49
|
+
res = subprocess.run(cmd, capture_output=True, text=True, encoding="utf-8", errors="replace", check=True)
|
|
50
|
+
parts = res.stdout.strip().split('|', 2)
|
|
51
|
+
if len(parts) == 3:
|
|
52
|
+
return {"author": parts[0], "date": parts[1], "message": parts[2]}
|
|
53
|
+
except:
|
|
54
|
+
pass
|
|
55
|
+
return {"author": "Desconhecido", "date": "Desconhecida", "message": "Sem mensagem"}
|
|
56
|
+
|
|
57
|
+
def analyze_commit_with_ai(commit_hash, file_path):
|
|
58
|
+
"""Usa a IA para ler o diff e classificar como ORIGEM ou REFATORACAO."""
|
|
59
|
+
diff = execute_git_show(commit_hash, file_path)
|
|
60
|
+
if not diff:
|
|
61
|
+
return {"status": "ORIGEM", "motivo": "Diff não encontrado (arquivo possivelmente criado aqui)."}
|
|
62
|
+
|
|
63
|
+
provider = get_ai_provider()
|
|
64
|
+
api_key = get_api_key(provider)
|
|
65
|
+
if not api_key:
|
|
66
|
+
return {"status": "ORIGEM", "motivo": "Sem chave de API. Assumindo origem."}
|
|
67
|
+
|
|
68
|
+
# Usamos o modelo 'simple' (Flash/Lite) para economizar dinheiro no loop
|
|
69
|
+
api_model = get_api_model(provider, task_complexity="simple")
|
|
70
|
+
|
|
71
|
+
skill_path = os.path.join(os.getcwd(), ".gitpr.blame.md")
|
|
72
|
+
if os.path.exists(skill_path):
|
|
73
|
+
with open(skill_path, "r", encoding="utf-8") as f:
|
|
74
|
+
sys_inst = f.read()
|
|
75
|
+
else:
|
|
76
|
+
sys_inst = 'Você é um Arquiteto de Software. Analise o diff e determine se é a ORIGEM da regra (lógica nova) ou REFATORAÇÃO. Responda APENAS com JSON: {"status": "ORIGEM", "motivo": "Explique o que foi introduzido"} ou {"status": "REFATORACAO", "motivo": "Explique o que foi alterado"}'
|
|
77
|
+
|
|
78
|
+
prompt = (
|
|
79
|
+
f"Analise o diff do commit {commit_hash} e retorne o JSON solicitado.\n\n"
|
|
80
|
+
f"DIFF:\n{diff[:4000]}" # Limitamos a 4000 caracteres para não pesar a requisição
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
click.secho(f" 🤖 Consultando a IA ({api_model}) sobre o commit {commit_hash[:8]}...", fg="cyan", dim=True)
|
|
84
|
+
|
|
85
|
+
result_json = call_ai_model(provider, api_key, api_model, prompt, sys_inst)
|
|
86
|
+
|
|
87
|
+
if result_json and "status" in result_json:
|
|
88
|
+
return result_json
|
|
89
|
+
|
|
90
|
+
return {"status": "ORIGEM", "motivo": "IA não retornou formato válido."}
|
|
91
|
+
|
|
92
|
+
def run_blame_analysis(file_path, start_line, end_line):
|
|
93
|
+
"""Motor de Loop Temporal que constrói a Timeline consolidada."""
|
|
94
|
+
click.secho(f"\n🔍 Iniciando Arqueologia de Código...", fg="cyan", bold=True)
|
|
95
|
+
click.echo(f"📍 Arquivo: {file_path} (Linhas: {start_line} até {end_line})")
|
|
96
|
+
|
|
97
|
+
initial_commits = execute_git_blame(file_path, start_line, end_line)
|
|
98
|
+
|
|
99
|
+
if not initial_commits:
|
|
100
|
+
click.secho("⚠️ Nenhum commit rastreável encontrado nestas linhas.", fg="yellow")
|
|
101
|
+
return
|
|
102
|
+
|
|
103
|
+
click.secho(f"✅ Encontrado(s) {len(initial_commits)} commit(s) na superfície. Iniciando viagem no tempo...\n", fg="green")
|
|
104
|
+
|
|
105
|
+
master_timeline = []
|
|
106
|
+
seen_hashes = set()
|
|
107
|
+
|
|
108
|
+
# LOOP DE COLETA DE DADOS
|
|
109
|
+
for base_commit in initial_commits:
|
|
110
|
+
current_commit = base_commit
|
|
111
|
+
depth = 0
|
|
112
|
+
max_depth = 4 # Trava de segurança para não rodar infinito em código legado
|
|
113
|
+
|
|
114
|
+
while depth < max_depth:
|
|
115
|
+
# Se já analisamos este commit em outra trilha, não gasta requisição à toa
|
|
116
|
+
if current_commit in seen_hashes:
|
|
117
|
+
break
|
|
118
|
+
|
|
119
|
+
seen_hashes.add(current_commit)
|
|
120
|
+
info = get_commit_info(current_commit)
|
|
121
|
+
ai_analysis = analyze_commit_with_ai(current_commit, file_path)
|
|
122
|
+
|
|
123
|
+
status = str(ai_analysis.get("status", "ORIGEM")).upper()
|
|
124
|
+
motivo = str(ai_analysis.get("motivo", ""))
|
|
125
|
+
|
|
126
|
+
master_timeline.append({
|
|
127
|
+
"hash": current_commit[:8],
|
|
128
|
+
"info": info,
|
|
129
|
+
"status": status,
|
|
130
|
+
"motivo": motivo,
|
|
131
|
+
"raw_date": info["date"] # Usado para ordenação
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
if status == "ORIGEM":
|
|
135
|
+
break
|
|
136
|
+
|
|
137
|
+
# É refatoração, vamos buscar o commit pai no passado
|
|
138
|
+
depth += 1
|
|
139
|
+
parent_hash = f"{current_commit}^"
|
|
140
|
+
parent_commits = execute_git_blame(file_path, start_line, end_line, parent_hash)
|
|
141
|
+
|
|
142
|
+
if not parent_commits:
|
|
143
|
+
break
|
|
144
|
+
current_commit = parent_commits[0]
|
|
145
|
+
|
|
146
|
+
# ORDENAÇÃO CRONOLÓGICA (Do mais antigo para o mais novo)
|
|
147
|
+
master_timeline.sort(key=lambda x: x["raw_date"])
|
|
148
|
+
|
|
149
|
+
# EXIBIÇÃO VISUAL NO TERMINAL (ÚNICA)
|
|
150
|
+
click.secho(f"\n📜 Histórico Consolidado da Regra (Linhas {start_line}-{end_line}):", fg="magenta", bold=True)
|
|
151
|
+
|
|
152
|
+
for item in master_timeline:
|
|
153
|
+
cor = "green" if item["status"] == "ORIGEM" else "yellow"
|
|
154
|
+
icone = "👶" if item["status"] == "ORIGEM" else "🔧"
|
|
155
|
+
|
|
156
|
+
click.secho(f"\n[{item['info']['date']}] {icone} {item['status']}: Por {item['info']['author']} (Commit: {item['hash']})", fg=cor, bold=True)
|
|
157
|
+
click.echo(f" └─ Mensagem: \"{item['info']['message']}\"")
|
|
158
|
+
if item["motivo"]:
|
|
159
|
+
click.secho(f" └─ Análise IA: {item['motivo']}", fg="cyan", dim=True)
|
|
160
|
+
|
|
161
|
+
click.echo("\n" + "-"*60 + "\n")
|
|
162
|
+
|
|
163
|
+
# GERAÇÃO DO RELATÓRIO MARKDOWN (ÚNICO)
|
|
164
|
+
click.secho("📝 Gerando relatório Markdown unificado com o resumo da IA...", fg="cyan")
|
|
165
|
+
|
|
166
|
+
branch_name = get_current_branch()
|
|
167
|
+
safe_branch_name = branch_name.replace("/", "-").replace("\\", "-")
|
|
168
|
+
current_time = datetime.now().strftime("%Y%m%d%H%M%S")
|
|
169
|
+
|
|
170
|
+
pattern = os.getenv("OUTPUT_FILE_NAME_BLAME", "{branch}_{datetime}_BLAME_REPORT.md")
|
|
171
|
+
output_filename = pattern.format(branch=safe_branch_name, datetime=current_time)
|
|
172
|
+
|
|
173
|
+
# Monta a Tabela Markdown
|
|
174
|
+
md_content = f"# Linha do tempo da regra investigada\n\n"
|
|
175
|
+
md_content += f"**Arquivo:** `{file_path}` (Linhas {start_line}-{end_line})\n\n"
|
|
176
|
+
md_content += "| Data | Commit | Autor | O quê |\n"
|
|
177
|
+
md_content += "|---|---|---|---|\n"
|
|
178
|
+
|
|
179
|
+
for item in master_timeline:
|
|
180
|
+
data_fmt = item['info']['date']
|
|
181
|
+
hash_curto = item['hash']
|
|
182
|
+
autor = item['info']['author']
|
|
183
|
+
msg_commit = item['info']['message']
|
|
184
|
+
|
|
185
|
+
# Pega a explicação da IA ou coloca um fallback seguro
|
|
186
|
+
explicacao_ia = item['motivo'] if item['motivo'] else "Alteração identificada na regra"
|
|
187
|
+
|
|
188
|
+
# Junta a explicação da IA com a mensagem do commit (Estilo Tabela de Referência)
|
|
189
|
+
motivo_final = f"{explicacao_ia} — *\"{msg_commit}\"*"
|
|
190
|
+
|
|
191
|
+
md_content += f"| {data_fmt} | `{hash_curto}` | {autor} | {motivo_final} |\n"
|
|
192
|
+
|
|
193
|
+
# IA gera o Resumo Analítico Final
|
|
194
|
+
summary_prompt = "Baseado na seguinte linha do tempo de commits de uma regra de negócio, escreva um único parágrafo resumindo a idade da regra, o autor original, o número de refatorações e deduza qual era a intenção original de negócio (o motivo real da regra existir no sistema).\n\n"
|
|
195
|
+
for item in master_timeline:
|
|
196
|
+
summary_prompt += f"[{item['info']['date']}] {item['info']['author']} ({item['hash']}) - {item['status']}: {item['motivo']}\n"
|
|
197
|
+
|
|
198
|
+
provider = get_ai_provider()
|
|
199
|
+
api_key = get_api_key(provider)
|
|
200
|
+
api_model = get_api_model(provider, task_complexity="advanced")
|
|
201
|
+
sys_inst = "Você é um Arquiteto de Software. Gere APENAS um objeto JSON no formato {\"resumo\": \"texto do resumo\"}."
|
|
202
|
+
|
|
203
|
+
click.secho(f" 🤖 Consultando a IA ({api_model}) para o Resumo Executivo...", fg="cyan", dim=True)
|
|
204
|
+
summary_json = call_ai_model(provider, api_key, api_model, summary_prompt, sys_inst)
|
|
205
|
+
|
|
206
|
+
resumo_texto = summary_json.get("resumo", "Resumo não disponível.") if summary_json else "Resumo não disponível."
|
|
207
|
+
|
|
208
|
+
md_content += f"\n**Resumo:** {resumo_texto}\n"
|
|
209
|
+
|
|
210
|
+
# Salva no disco
|
|
211
|
+
try:
|
|
212
|
+
with open(output_filename, "w", encoding="utf-8") as f:
|
|
213
|
+
f.write(md_content)
|
|
214
|
+
click.secho(f"✅ Relatório unificado salvo com sucesso: '{output_filename}'", fg="green", bold=True)
|
|
215
|
+
except Exception as e:
|
|
216
|
+
click.secho(f"❌ Erro ao salvar o relatório: {e}", fg="red")
|
|
@@ -20,7 +20,8 @@ DEFAULT_CONFIG = {
|
|
|
20
20
|
"OUTPUT_FILE_NAME": "{branch}_{datetime}_PR_DESC.md",
|
|
21
21
|
"OUTPUT_FILE_NAME_REVIEW": "{branch}_{datetime}_PR_REVIEW.txt",
|
|
22
22
|
"OUTPUT_FILE_NAME_FULLREVIEW": "{branch}_{datetime}_PR_FULLREVIEW.txt",
|
|
23
|
-
"OUTPUT_FILE_NAME_FILEREVIEW": "{branch}_{datetime}_FILE_REVIEW.txt"
|
|
23
|
+
"OUTPUT_FILE_NAME_FILEREVIEW": "{branch}_{datetime}_FILE_REVIEW.txt",
|
|
24
|
+
"OUTPUT_FILE_NAME_BLAME": "{branch}_{datetime}_BLAME_REPORT.md"
|
|
24
25
|
}
|
|
25
26
|
|
|
26
27
|
def get_ai_provider():
|
|
@@ -5,7 +5,7 @@ import sys
|
|
|
5
5
|
|
|
6
6
|
# Importações dos nossos módulos internos atualizadas
|
|
7
7
|
from src.config import setup_environment, check_internet_connection, get_ai_provider
|
|
8
|
-
from src.updater import check_and_update, __version__
|
|
8
|
+
from src.updater import check_and_update, __version__, print_update_notice
|
|
9
9
|
from src.core import (
|
|
10
10
|
get_git_diff,
|
|
11
11
|
get_git_full_diff,
|
|
@@ -45,8 +45,9 @@ def print_banner():
|
|
|
45
45
|
@click.option('--hook', type=click.Path(), hidden=True, help="Caminho do arquivo de commit (uso interno dos hooks).")
|
|
46
46
|
@click.option('-q', '--quiet', is_flag=True, hidden=True, help="Oculta o banner e logs não essenciais (uso interno).")
|
|
47
47
|
@click.option('-i', '--input', type=click.Path(exists=True), help="Caminho de um arquivo específico para análise completa.")
|
|
48
|
+
@click.option('-b', '--blame', type=str, help="Analisa a origem de uma regra de negócio (ex: arquivo.py:10-20 ou apenas arquivo.py).")
|
|
48
49
|
@click.option('-p', '--provider', type=click.Choice(['gemini', 'deepseek']), help="Força a utilização de um provedor de IA específico nesta execução.")
|
|
49
|
-
def cli(commit, review, fullreview, linter, skill, update, installhooks, hook, quiet, provider, input):
|
|
50
|
+
def cli(commit, review, fullreview, linter, skill, update, installhooks, hook, quiet, provider, input, blame):
|
|
50
51
|
"""
|
|
51
52
|
GitPR CLI - Automação de PRs e Code Review com IA.
|
|
52
53
|
|
|
@@ -109,17 +110,10 @@ def cli(commit, review, fullreview, linter, skill, update, installhooks, hook, q
|
|
|
109
110
|
check_internet_connection()
|
|
110
111
|
|
|
111
112
|
# Módulo de Atualização (Pip-Aware)
|
|
112
|
-
if update:
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
check_and_update()
|
|
116
|
-
else:
|
|
117
|
-
click.secho("💡 Como você instalou via PIP, atualize rodando: pip install --upgrade gitpr-cli", fg="cyan", bold=True)
|
|
113
|
+
if update:
|
|
114
|
+
click.secho("🔍 Verificando atualizações...", fg="cyan")
|
|
115
|
+
check_and_update()
|
|
118
116
|
return
|
|
119
|
-
else:
|
|
120
|
-
# Verificação automática em segundo plano a cada uso (Apenas binário)
|
|
121
|
-
if is_compiled:
|
|
122
|
-
check_and_update()
|
|
123
117
|
|
|
124
118
|
# Opção --skill: Gera o template e encerra
|
|
125
119
|
if skill:
|
|
@@ -144,6 +138,45 @@ def cli(commit, review, fullreview, linter, skill, update, installhooks, hook, q
|
|
|
144
138
|
click.echo("---\n")
|
|
145
139
|
return
|
|
146
140
|
|
|
141
|
+
# Módulo Arqueólogo (--blame)
|
|
142
|
+
if blame:
|
|
143
|
+
# Parser para separar o arquivo das linhas
|
|
144
|
+
if ":" in blame:
|
|
145
|
+
# Modo Direto: gitpr --blame arquivo:10-20
|
|
146
|
+
file_path, lines = blame.split(":", 1)
|
|
147
|
+
try:
|
|
148
|
+
if "-" in lines:
|
|
149
|
+
start_line, end_line = lines.split("-")
|
|
150
|
+
else:
|
|
151
|
+
start_line = end_line = lines
|
|
152
|
+
except ValueError:
|
|
153
|
+
click.secho("❌ Formato de linhas inválido. Use inicio-fim (ex: 10-20).", fg="red")
|
|
154
|
+
return
|
|
155
|
+
else:
|
|
156
|
+
# Modo Interativo: gitpr --blame arquivo
|
|
157
|
+
file_path = blame
|
|
158
|
+
if not os.path.exists(file_path):
|
|
159
|
+
click.secho(f"❌ O arquivo '{file_path}' não foi encontrado.", fg="red")
|
|
160
|
+
return
|
|
161
|
+
|
|
162
|
+
click.secho(f"📂 Arquivo selecionado: {file_path}", fg="cyan", bold=True)
|
|
163
|
+
lines_input = click.prompt("Quais linhas você deseja investigar? (Ex: 10-20 ou apenas 45)")
|
|
164
|
+
|
|
165
|
+
if "-" in lines_input:
|
|
166
|
+
start_line, end_line = lines_input.split("-")
|
|
167
|
+
else:
|
|
168
|
+
start_line = end_line = lines_input
|
|
169
|
+
|
|
170
|
+
# Validação final do arquivo
|
|
171
|
+
if not os.path.exists(file_path):
|
|
172
|
+
click.secho(f"❌ O arquivo '{file_path}' não foi encontrado.", fg="red")
|
|
173
|
+
return
|
|
174
|
+
|
|
175
|
+
# Aciona o motor
|
|
176
|
+
from src.blame_engine import run_blame_analysis
|
|
177
|
+
run_blame_analysis(file_path.strip(), start_line.strip(), end_line.strip())
|
|
178
|
+
return
|
|
179
|
+
|
|
147
180
|
# Validação do Modo Input
|
|
148
181
|
if input and not (review or fullreview):
|
|
149
182
|
click.secho("\n❌ Erro: A opção --input (-i) só pode ser utilizada em conjunto com --review (-r) ou --fullreview (-f).", fg="red", bold=True)
|
|
@@ -294,6 +327,9 @@ def cli(commit, review, fullreview, linter, skill, update, installhooks, hook, q
|
|
|
294
327
|
click.secho(f"\n✅ Sucesso! O arquivo '{output_filename}' foi gerado na pasta atual.", fg="green", bold=True)
|
|
295
328
|
except Exception as e:
|
|
296
329
|
click.secho(f"\n❌ Erro ao guardar o arquivo: {e}", fg="red")
|
|
330
|
+
|
|
331
|
+
if not quiet:
|
|
332
|
+
print_update_notice()
|
|
297
333
|
|
|
298
334
|
if __name__ == "__main__":
|
|
299
335
|
cli()
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import urllib.request
|
|
2
|
+
import json
|
|
3
|
+
import os
|
|
4
|
+
import sys
|
|
5
|
+
import shutil
|
|
6
|
+
import click
|
|
7
|
+
from datetime import datetime
|
|
8
|
+
|
|
9
|
+
# Versão atual do seu executável local (Atualize isso a cada novo build!)
|
|
10
|
+
__version__ = "0.0.14"
|
|
11
|
+
GITHUB_API_URL = "https://api.github.com/repos/natanfiuza/gitpr/releases/latest"
|
|
12
|
+
PYPI_API_URL = "https://pypi.org/pypi/gitpr-cli/json"
|
|
13
|
+
|
|
14
|
+
def get_gitpr_dir():
|
|
15
|
+
"""Retorna o diretório ~/.gitpr/"""
|
|
16
|
+
return os.path.join(os.path.expanduser("~"), ".gitpr")
|
|
17
|
+
|
|
18
|
+
def get_update_cache_file():
|
|
19
|
+
"""Retorna o caminho do arquivo de cache de atualizações."""
|
|
20
|
+
return os.path.join(get_gitpr_dir(), "update_cache.json")
|
|
21
|
+
|
|
22
|
+
def parse_version(version_str):
|
|
23
|
+
"""Converte 'v0.1.0' ou '0.1.0' em uma tupla (0, 1, 0) para matemática de versões."""
|
|
24
|
+
clean_version = version_str.lower().replace("v", "")
|
|
25
|
+
try:
|
|
26
|
+
return tuple(map(int, clean_version.split(".")))
|
|
27
|
+
except ValueError:
|
|
28
|
+
return (0, 0, 0)
|
|
29
|
+
|
|
30
|
+
def get_latest_remote_version(is_compiled):
|
|
31
|
+
"""Busca a última versão na API correta (PyPI ou GitHub) com cache diário."""
|
|
32
|
+
cache_file = get_update_cache_file()
|
|
33
|
+
today = datetime.now().strftime("%Y-%m-%d")
|
|
34
|
+
|
|
35
|
+
# 1. Tenta ler do cache para não atrasar o terminal do usuário
|
|
36
|
+
if os.path.exists(cache_file):
|
|
37
|
+
try:
|
|
38
|
+
with open(cache_file, "r") as f:
|
|
39
|
+
cache_data = json.load(f)
|
|
40
|
+
if cache_data.get("date") == today:
|
|
41
|
+
return cache_data.get("version"), cache_data.get("download_url")
|
|
42
|
+
except Exception:
|
|
43
|
+
pass
|
|
44
|
+
|
|
45
|
+
# 2. Busca na Web se o cache expirou
|
|
46
|
+
latest_version = ""
|
|
47
|
+
download_url = ""
|
|
48
|
+
|
|
49
|
+
try:
|
|
50
|
+
if is_compiled:
|
|
51
|
+
# Busca no GitHub (Para executável)
|
|
52
|
+
req = urllib.request.Request(GITHUB_API_URL, headers={'User-Agent': 'GitPR-Updater'})
|
|
53
|
+
with urllib.request.urlopen(req, timeout=3) as response:
|
|
54
|
+
data = json.loads(response.read().decode())
|
|
55
|
+
latest_version = data.get("tag_name", "").replace("v", "")
|
|
56
|
+
|
|
57
|
+
assets = data.get("assets", [])
|
|
58
|
+
exe_asset = next((a for a in assets if a.get("name") == "gitpr.exe"), None)
|
|
59
|
+
if exe_asset:
|
|
60
|
+
download_url = exe_asset.get("browser_download_url")
|
|
61
|
+
else:
|
|
62
|
+
# Busca no PyPI (Para instalação via PIP)
|
|
63
|
+
req = urllib.request.Request(PYPI_API_URL, headers={'User-Agent': 'GitPR-Updater'})
|
|
64
|
+
with urllib.request.urlopen(req, timeout=3) as response:
|
|
65
|
+
data = json.loads(response.read().decode())
|
|
66
|
+
latest_version = data.get("info", {}).get("version", "")
|
|
67
|
+
|
|
68
|
+
# 3. Salva no cache
|
|
69
|
+
if latest_version:
|
|
70
|
+
os.makedirs(get_gitpr_dir(), exist_ok=True)
|
|
71
|
+
with open(cache_file, "w") as f:
|
|
72
|
+
json.dump({"date": today, "version": latest_version, "download_url": download_url}, f)
|
|
73
|
+
|
|
74
|
+
except Exception:
|
|
75
|
+
pass # Falha silenciosa em caso de falta de internet
|
|
76
|
+
|
|
77
|
+
return latest_version, download_url
|
|
78
|
+
|
|
79
|
+
def print_update_notice():
|
|
80
|
+
"""Imprime o bloco de aviso estilo PIP no fim da execução."""
|
|
81
|
+
is_compiled = getattr(sys, 'frozen', False)
|
|
82
|
+
latest_version, _ = get_latest_remote_version(is_compiled)
|
|
83
|
+
|
|
84
|
+
if not latest_version:
|
|
85
|
+
return
|
|
86
|
+
|
|
87
|
+
current_v = parse_version(__version__)
|
|
88
|
+
latest_v = parse_version(latest_version)
|
|
89
|
+
|
|
90
|
+
if latest_v > current_v:
|
|
91
|
+
click.echo("")
|
|
92
|
+
click.secho(f"[notice] A new release of gitpr is available: {__version__} -> {latest_version}", fg="yellow", dim=True)
|
|
93
|
+
if is_compiled:
|
|
94
|
+
click.secho(f"[notice] To update, run: gitpr --update", fg="yellow", dim=True)
|
|
95
|
+
else:
|
|
96
|
+
click.secho(f"[notice] To update, run: pip install --upgrade gitpr-cli", fg="yellow", dim=True)
|
|
97
|
+
click.echo("")
|
|
98
|
+
|
|
99
|
+
def check_and_update():
|
|
100
|
+
"""Função disparada apenas quando o usuário força a flag --update."""
|
|
101
|
+
is_compiled = getattr(sys, 'frozen', False)
|
|
102
|
+
|
|
103
|
+
if not is_compiled:
|
|
104
|
+
click.secho("💡 Como você instalou via PIP, atualize rodando: pip install --upgrade gitpr-cli", fg="cyan", bold=True)
|
|
105
|
+
return
|
|
106
|
+
|
|
107
|
+
latest_version, download_url = get_latest_remote_version(is_compiled=True)
|
|
108
|
+
|
|
109
|
+
if not latest_version or not download_url:
|
|
110
|
+
click.secho("❌ Não foi possível verificar atualizações no momento.", fg="red")
|
|
111
|
+
return
|
|
112
|
+
|
|
113
|
+
current_v = parse_version(__version__)
|
|
114
|
+
latest_v = parse_version(latest_version)
|
|
115
|
+
|
|
116
|
+
if latest_v > current_v:
|
|
117
|
+
click.secho(f"\n🚀 Nova versão do GitPR encontrada (v{latest_version})!", fg="green", bold=True)
|
|
118
|
+
click.secho("Baixando atualização em segundo plano...", fg="cyan")
|
|
119
|
+
_perform_hot_swap(download_url)
|
|
120
|
+
else:
|
|
121
|
+
click.secho("✅ Você já está usando a versão mais recente do GitPR.", fg="green")
|
|
122
|
+
|
|
123
|
+
def _perform_hot_swap(download_url):
|
|
124
|
+
"""Faz o download e substitui o executável atual."""
|
|
125
|
+
current_exe = sys.executable
|
|
126
|
+
old_exe = current_exe + ".old"
|
|
127
|
+
|
|
128
|
+
try:
|
|
129
|
+
if os.path.exists(old_exe):
|
|
130
|
+
os.remove(old_exe)
|
|
131
|
+
os.rename(current_exe, old_exe)
|
|
132
|
+
urllib.request.urlretrieve(download_url, current_exe)
|
|
133
|
+
click.secho(f"✅ Atualização concluída com sucesso! Na próxima execução você já usará a nova versão.\n", fg="green", bold=True)
|
|
134
|
+
except Exception as e:
|
|
135
|
+
click.secho(f"❌ Falha ao aplicar atualização: {e}", fg="red")
|
|
136
|
+
if os.path.exists(old_exe) and not os.path.exists(current_exe):
|
|
137
|
+
os.rename(old_exe, current_exe)
|
|
138
|
+
"""Faz o download e substitui o executável atual (Hot-Swap)."""
|
|
139
|
+
current_exe = sys.executable
|
|
140
|
+
|
|
141
|
+
# Se não estiver rodando como executável compilado (PyInstaller), aborta o update
|
|
142
|
+
if not getattr(sys, 'frozen', False):
|
|
143
|
+
click.secho("⚠️ Aviso: Rodando via script Python. O Auto-Update funciona apenas no executável compilado.", fg="yellow")
|
|
144
|
+
return
|
|
145
|
+
|
|
146
|
+
old_exe = current_exe + ".old"
|
|
147
|
+
|
|
148
|
+
try:
|
|
149
|
+
# 1. Renomeia o executável atual que está em uso
|
|
150
|
+
if os.path.exists(old_exe):
|
|
151
|
+
os.remove(old_exe) # Remove restos antigos se existirem
|
|
152
|
+
os.rename(current_exe, old_exe)
|
|
153
|
+
|
|
154
|
+
# 2. Faz o download direto para o caminho original
|
|
155
|
+
urllib.request.urlretrieve(download_url, current_exe)
|
|
156
|
+
|
|
157
|
+
# 3. Salva o novo hash
|
|
158
|
+
with open(sha_file, "w") as f:
|
|
159
|
+
f.write(new_digest)
|
|
160
|
+
|
|
161
|
+
click.secho(f"✅ Atualização concluída com sucesso! Na próxima execução você já usará a nova versão.\n", fg="green", bold=True)
|
|
162
|
+
|
|
163
|
+
except Exception as e:
|
|
164
|
+
# Se algo falhar na renomeação/download, tenta desfazer a bagunça
|
|
165
|
+
click.secho(f"❌ Falha ao aplicar atualização: {e}", fg="red")
|
|
166
|
+
if os.path.exists(old_exe) and not os.path.exists(current_exe):
|
|
167
|
+
os.rename(old_exe, current_exe)
|
gitpr_cli-0.0.12/src/updater.py
DELETED
|
@@ -1,90 +0,0 @@
|
|
|
1
|
-
import urllib.request
|
|
2
|
-
import json
|
|
3
|
-
import os
|
|
4
|
-
import sys
|
|
5
|
-
import shutil
|
|
6
|
-
import click
|
|
7
|
-
|
|
8
|
-
# Versão atual do seu executável local (Atualize isso a cada novo build!)
|
|
9
|
-
__version__ = "0.0.12"
|
|
10
|
-
GITHUB_API_URL = "https://api.github.com/repos/natanfiuza/gitpr/releases/latest"
|
|
11
|
-
|
|
12
|
-
def get_gitpr_dir():
|
|
13
|
-
"""Retorna o diretório ~/.gitpr/"""
|
|
14
|
-
return os.path.join(os.path.expanduser("~"), ".gitpr")
|
|
15
|
-
|
|
16
|
-
def check_and_update():
|
|
17
|
-
"""Verifica na API do GitHub se há um novo release com um hash diferente."""
|
|
18
|
-
try:
|
|
19
|
-
# Timeout curto para não travar a CLI se a API do GitHub estiver lenta
|
|
20
|
-
req = urllib.request.Request(GITHUB_API_URL, headers={'User-Agent': 'GitPR-Updater'})
|
|
21
|
-
with urllib.request.urlopen(req, timeout=3) as response:
|
|
22
|
-
data = json.loads(response.read().decode())
|
|
23
|
-
|
|
24
|
-
# Pega a versão remota (apenas para log/exibição)
|
|
25
|
-
remote_version = data.get("tag_name", "").replace("v", "")
|
|
26
|
-
|
|
27
|
-
# Procura o executável nos assets
|
|
28
|
-
assets = data.get("assets", [])
|
|
29
|
-
exe_asset = next((a for a in assets if a.get("name") == "gitpr.exe"), None)
|
|
30
|
-
|
|
31
|
-
if not exe_asset:
|
|
32
|
-
return # Nenhum executável encontrado no release mais recente
|
|
33
|
-
|
|
34
|
-
remote_digest = exe_asset.get("digest", "").replace("sha256:", "")
|
|
35
|
-
download_url = exe_asset.get("browser_download_url")
|
|
36
|
-
|
|
37
|
-
if not remote_digest or not download_url:
|
|
38
|
-
return
|
|
39
|
-
|
|
40
|
-
# Compara com o hash local
|
|
41
|
-
gitpr_dir = get_gitpr_dir()
|
|
42
|
-
sha_file = os.path.join(gitpr_dir, ".sha256")
|
|
43
|
-
local_digest = ""
|
|
44
|
-
|
|
45
|
-
if os.path.exists(sha_file):
|
|
46
|
-
with open(sha_file, "r") as f:
|
|
47
|
-
local_digest = f.read().strip()
|
|
48
|
-
|
|
49
|
-
# Se os hashes forem diferentes, dispara o update!
|
|
50
|
-
if remote_digest != local_digest:
|
|
51
|
-
click.secho(f"\n🚀 Nova versão do GitPR encontrada (v{remote_version})!", fg="green", bold=True)
|
|
52
|
-
click.secho("Baixando atualização em segundo plano...", fg="cyan")
|
|
53
|
-
_perform_hot_swap(download_url, remote_digest, sha_file)
|
|
54
|
-
|
|
55
|
-
except Exception as e:
|
|
56
|
-
# Silencia erros de timeout ou rede para não atrapalhar o fluxo do usuário
|
|
57
|
-
click.secho(f"Debug Updater: {e}", fg="red") # Descomente para debugar
|
|
58
|
-
pass
|
|
59
|
-
|
|
60
|
-
def _perform_hot_swap(download_url, new_digest, sha_file):
|
|
61
|
-
"""Faz o download e substitui o executável atual (Hot-Swap)."""
|
|
62
|
-
current_exe = sys.executable
|
|
63
|
-
|
|
64
|
-
# Se não estiver rodando como executável compilado (PyInstaller), aborta o update
|
|
65
|
-
if not getattr(sys, 'frozen', False):
|
|
66
|
-
click.secho("⚠️ Aviso: Rodando via script Python. O Auto-Update funciona apenas no executável compilado.", fg="yellow")
|
|
67
|
-
return
|
|
68
|
-
|
|
69
|
-
old_exe = current_exe + ".old"
|
|
70
|
-
|
|
71
|
-
try:
|
|
72
|
-
# 1. Renomeia o executável atual que está em uso
|
|
73
|
-
if os.path.exists(old_exe):
|
|
74
|
-
os.remove(old_exe) # Remove restos antigos se existirem
|
|
75
|
-
os.rename(current_exe, old_exe)
|
|
76
|
-
|
|
77
|
-
# 2. Faz o download direto para o caminho original
|
|
78
|
-
urllib.request.urlretrieve(download_url, current_exe)
|
|
79
|
-
|
|
80
|
-
# 3. Salva o novo hash
|
|
81
|
-
with open(sha_file, "w") as f:
|
|
82
|
-
f.write(new_digest)
|
|
83
|
-
|
|
84
|
-
click.secho(f"✅ Atualização concluída com sucesso! Na próxima execução você já usará a nova versão.\n", fg="green", bold=True)
|
|
85
|
-
|
|
86
|
-
except Exception as e:
|
|
87
|
-
# Se algo falhar na renomeação/download, tenta desfazer a bagunça
|
|
88
|
-
click.secho(f"❌ Falha ao aplicar atualização: {e}", fg="red")
|
|
89
|
-
if os.path.exists(old_exe) and not os.path.exists(current_exe):
|
|
90
|
-
os.rename(old_exe, current_exe)
|
|
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
|