gitpr-cli 0.0.11__py3-none-any.whl
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.
- __init__.py +1 -0
- ai_providers.py +65 -0
- cache.py +50 -0
- config.py +154 -0
- core.py +305 -0
- gitpr_cli-0.0.11.dist-info/METADATA +211 -0
- gitpr_cli-0.0.11.dist-info/RECORD +15 -0
- gitpr_cli-0.0.11.dist-info/WHEEL +5 -0
- gitpr_cli-0.0.11.dist-info/entry_points.txt +2 -0
- gitpr_cli-0.0.11.dist-info/licenses/LICENSE +502 -0
- gitpr_cli-0.0.11.dist-info/top_level.txt +9 -0
- linter_engine.py +122 -0
- main.py +299 -0
- security.py +42 -0
- updater.py +90 -0
main.py
ADDED
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from datetime import datetime
|
|
3
|
+
import click
|
|
4
|
+
import sys
|
|
5
|
+
|
|
6
|
+
# Importações dos nossos módulos internos atualizadas
|
|
7
|
+
from src.config import setup_environment, check_internet_connection, get_ai_provider
|
|
8
|
+
from src.updater import check_and_update, __version__
|
|
9
|
+
from src.core import (
|
|
10
|
+
get_git_diff,
|
|
11
|
+
get_git_full_diff,
|
|
12
|
+
get_current_branch,
|
|
13
|
+
generate_pr_content,
|
|
14
|
+
generate_skill_template,
|
|
15
|
+
install_git_hooks
|
|
16
|
+
)
|
|
17
|
+
from src.linter_engine import parse_diff_and_lint
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def print_banner():
|
|
21
|
+
"""Exibe a assinatura ASCII Art do projeto"""
|
|
22
|
+
banner = """
|
|
23
|
+
,----. ,--. ,--. ,------. ,------.
|
|
24
|
+
' .-./ `--',-' '-.| .--. '| .--. '
|
|
25
|
+
| | .---.,--.'-. .-'| '--' || '--'.'
|
|
26
|
+
' '--' || | | | | | --' | |\ \
|
|
27
|
+
`------' `--' `--' `--' `--' '--'
|
|
28
|
+
|
|
29
|
+
"""
|
|
30
|
+
click.secho(banner, fg="cyan", bold=True)
|
|
31
|
+
click.secho(f" 🚀 Automação Inteligente de PRs com IA (v{__version__})", fg="yellow", bold=True)
|
|
32
|
+
click.secho(" Opções: -c,--commit | -r,--review | -f,--fullreview | -l,--linter | -s,--skill | -u,--update | -ih,--installhooks | -h ou --help\n", fg="white", dim=True)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
# Configuração nativa do Click para aceitar -h além de --help
|
|
36
|
+
@click.command()
|
|
37
|
+
@click.help_option('-h', '--help', help='Mostra esta mensagem e sai.')
|
|
38
|
+
@click.option('-c', '--commit', is_flag=True, help="Gera apenas a mensagem de commit e exibe no console.")
|
|
39
|
+
@click.option('-r', '--review', is_flag=True, help="Faz um code review das alterações locais (git diff).")
|
|
40
|
+
@click.option('-f', '--fullreview', is_flag=True, help="Faz um code review de todas as alterações desde a branch principal (origin/main).")
|
|
41
|
+
@click.option('-l', '--linter', is_flag=True, help="Roda apenas o linter estático local (ideal para CI/CD).")
|
|
42
|
+
@click.option('-s', '--skill', is_flag=True, help="Gera o arquivo de template .gitpr.md na pasta atual.")
|
|
43
|
+
@click.option('-u', '--update', is_flag=True, help="Verifica e instala a versão mais recente do GitPR.")
|
|
44
|
+
@click.option('-ih', '--installhooks', is_flag=True, help="Instala automaticamente os Git Hooks de validação no projeto.")
|
|
45
|
+
@click.option('--hook', type=click.Path(), hidden=True, help="Caminho do arquivo de commit (uso interno dos hooks).")
|
|
46
|
+
@click.option('-q', '--quiet', is_flag=True, hidden=True, help="Oculta o banner e logs não essenciais (uso interno).")
|
|
47
|
+
@click.option('-i', '--input', type=click.Path(exists=True), help="Caminho de um arquivo específico para análise completa.")
|
|
48
|
+
@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
|
+
"""
|
|
51
|
+
GitPR CLI - Automação de PRs e Code Review com IA.
|
|
52
|
+
|
|
53
|
+
COMPORTAMENTO PADRÃO (Sem opções):
|
|
54
|
+
Faz o fetch, compara com a branch principal remota e gera um arquivo Markdown (.md) com a descrição completa para o Pull Request.
|
|
55
|
+
"""
|
|
56
|
+
# Silencia o banner se estiver no modo quiet ou via hook
|
|
57
|
+
if not quiet and not hook:
|
|
58
|
+
print_banner()
|
|
59
|
+
|
|
60
|
+
# Identifica se a ferramenta está rodando como binário (PyInstaller) ou via PIP
|
|
61
|
+
is_compiled = getattr(sys, 'frozen', False)
|
|
62
|
+
|
|
63
|
+
# Limpeza do Hot-Swap (Apenas no modo Binário)
|
|
64
|
+
if is_compiled:
|
|
65
|
+
old_exe = sys.executable + ".old"
|
|
66
|
+
if os.path.exists(old_exe):
|
|
67
|
+
try:
|
|
68
|
+
os.remove(old_exe)
|
|
69
|
+
except OSError:
|
|
70
|
+
pass
|
|
71
|
+
|
|
72
|
+
if linter:
|
|
73
|
+
diff_text = get_git_diff()
|
|
74
|
+
|
|
75
|
+
if not diff_text or not diff_text.strip():
|
|
76
|
+
if not quiet: click.secho("✅ Nada para validar (diff vazio).", fg="green")
|
|
77
|
+
return
|
|
78
|
+
|
|
79
|
+
linter_results = parse_diff_and_lint(diff_text)
|
|
80
|
+
|
|
81
|
+
has_warnings = len(linter_results["warnings"]) > 0
|
|
82
|
+
has_errors = len(linter_results["errors"]) > 0
|
|
83
|
+
|
|
84
|
+
# Processamento de Warnings (Apenas Avisos)
|
|
85
|
+
if has_warnings:
|
|
86
|
+
# Os avisos DEVEM aparecer sempre, mesmo no modo quiet
|
|
87
|
+
click.secho(f"\n⚠️ O Linter gerou {len(linter_results['warnings'])} aviso(s) de boas práticas:", fg="yellow", bold=True)
|
|
88
|
+
for alert in linter_results["warnings"]:
|
|
89
|
+
click.echo(f" - {alert}")
|
|
90
|
+
|
|
91
|
+
# Processamento de Erros (Críticos, Bloqueiam o Commit)
|
|
92
|
+
if has_errors:
|
|
93
|
+
# Os erros DEVEM aparecer sempre, mesmo no modo quiet
|
|
94
|
+
click.secho(f"\n🚨 Falha na validação! Encontrados {len(linter_results['errors'])} erro(s) críticos:", fg="red", bold=True)
|
|
95
|
+
for alert in linter_results["errors"]:
|
|
96
|
+
click.echo(f" - {alert}")
|
|
97
|
+
# Trava o Git apenas se houver erros críticos
|
|
98
|
+
sys.exit(1)
|
|
99
|
+
|
|
100
|
+
# Sucesso silencioso (Nenhum erro crítico encontrado)
|
|
101
|
+
if not quiet:
|
|
102
|
+
if has_warnings:
|
|
103
|
+
click.secho("\n✅ Código aprovado com avisos. O commit prosseguirá.", fg="green")
|
|
104
|
+
else:
|
|
105
|
+
click.secho("\n✅ Código limpo! Nenhuma violação encontrada pelo Linter local.", fg="green", bold=True)
|
|
106
|
+
return
|
|
107
|
+
|
|
108
|
+
# Guardião de Conexão (Failing Fast)
|
|
109
|
+
check_internet_connection()
|
|
110
|
+
|
|
111
|
+
# Módulo de Atualização (Pip-Aware)
|
|
112
|
+
if update:
|
|
113
|
+
if is_compiled:
|
|
114
|
+
click.secho("🔍 Verificando atualizações no GitHub...", fg="cyan")
|
|
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)
|
|
118
|
+
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
|
+
|
|
124
|
+
# Opção --skill: Gera o template e encerra
|
|
125
|
+
if skill:
|
|
126
|
+
generate_skill_template()
|
|
127
|
+
return
|
|
128
|
+
|
|
129
|
+
if installhooks:
|
|
130
|
+
if install_git_hooks():
|
|
131
|
+
click.secho("\n✅ Git Hooks instalados com sucesso!", fg="green", bold=True)
|
|
132
|
+
click.echo("O Linter será agora executado automaticamente antes de cada commit.")
|
|
133
|
+
|
|
134
|
+
click.echo("\n---")
|
|
135
|
+
click.echo("📚 Guias de Utilização:")
|
|
136
|
+
|
|
137
|
+
# Link da documentação geral de Hooks
|
|
138
|
+
click.echo("• Como utilizar Git Hooks:")
|
|
139
|
+
click.secho(" https://github.com/natanfiuza/gitpr/blob/main/docs/git-hooks-locais.md", fg="blue")
|
|
140
|
+
|
|
141
|
+
# Novo link: Documentação de Regras Customizadas
|
|
142
|
+
click.echo("• Como criar novas regras de Linter (.gitpr.linter.yml):")
|
|
143
|
+
click.secho(" https://github.com/natanfiuza/gitpr/blob/main/docs/linter-regras-customizadas.md", fg="blue")
|
|
144
|
+
click.echo("---\n")
|
|
145
|
+
return
|
|
146
|
+
|
|
147
|
+
# Validação do Modo Input
|
|
148
|
+
if input and not (review or fullreview):
|
|
149
|
+
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)
|
|
150
|
+
return
|
|
151
|
+
|
|
152
|
+
# Garante que o ambiente e as chaves estão configurados
|
|
153
|
+
setup_environment()
|
|
154
|
+
|
|
155
|
+
# Determina o provedor de IA a ser usado (opção de linha de comando tem prioridade)
|
|
156
|
+
active_provider = provider if provider else get_ai_provider()
|
|
157
|
+
|
|
158
|
+
# Determina o tipo de ação e qual diff capturar
|
|
159
|
+
action_type = "pr"
|
|
160
|
+
diff_text = ""
|
|
161
|
+
|
|
162
|
+
if input:
|
|
163
|
+
# MODO FILE REVIEW: Lê o arquivo físico em vez do git diff
|
|
164
|
+
action_type = "filereview"
|
|
165
|
+
try:
|
|
166
|
+
with open(input, "r", encoding="utf-8") as f:
|
|
167
|
+
diff_text = f.read()
|
|
168
|
+
click.secho(f"📄 Modo Arquivo: Analisando conteúdo integral de '{input}'...", fg="blue")
|
|
169
|
+
except Exception as e:
|
|
170
|
+
click.secho(f"❌ Erro ao ler o arquivo: {e}", fg="red")
|
|
171
|
+
return
|
|
172
|
+
elif commit:
|
|
173
|
+
action_type = "commit"
|
|
174
|
+
diff_text = get_git_diff()
|
|
175
|
+
elif review:
|
|
176
|
+
action_type = "review"
|
|
177
|
+
diff_text = get_git_diff()
|
|
178
|
+
elif fullreview:
|
|
179
|
+
action_type = "fullreview"
|
|
180
|
+
diff_text = get_git_full_diff()
|
|
181
|
+
else:
|
|
182
|
+
# Padrão: Descrição de PR usando o Full Diff contra a main remota
|
|
183
|
+
action_type = "pr"
|
|
184
|
+
diff_text = get_git_full_diff()
|
|
185
|
+
|
|
186
|
+
# CRÍTICO: Avisa o usuário antes de sair se não houver alterações
|
|
187
|
+
if not diff_text or not diff_text.strip():
|
|
188
|
+
click.secho("\n⚠️ Nenhum código novo encontrado. Faça alguma alteração ou verifique sua branch antes de rodar o comando.\n", fg="yellow")
|
|
189
|
+
return
|
|
190
|
+
|
|
191
|
+
# Chama a IA de acordo com active_provider utilizando a nova assinatura da função
|
|
192
|
+
# A assinatura requer: action_folder, action_type, diff_text, provider
|
|
193
|
+
# Usamos o próprio action_type como action_folder, pois a função lida com isso internamente.
|
|
194
|
+
data = generate_pr_content(action_type, action_type, diff_text, active_provider)
|
|
195
|
+
if not data:
|
|
196
|
+
return
|
|
197
|
+
|
|
198
|
+
# Processamento da Saída
|
|
199
|
+
branch_name = get_current_branch()
|
|
200
|
+
safe_branch_name = branch_name.replace("/", "-").replace("\\", "-")
|
|
201
|
+
current_time = datetime.now().strftime("%Y%m%d%H%M%S")
|
|
202
|
+
|
|
203
|
+
# Apenas Commit no console
|
|
204
|
+
if action_type == "commit":
|
|
205
|
+
msg = data.get('commit_message', 'Atualização de código')
|
|
206
|
+
|
|
207
|
+
if hook:
|
|
208
|
+
# MODO HOOK: Injeta a mensagem direto no arquivo do Git
|
|
209
|
+
try:
|
|
210
|
+
with open(hook, "r", encoding="utf-8") as f:
|
|
211
|
+
original_content = f.read()
|
|
212
|
+
|
|
213
|
+
# Coloca a sugestão no topo, mantendo os comentários originais do Git abaixo
|
|
214
|
+
with open(hook, "w", encoding="utf-8") as f:
|
|
215
|
+
f.write(f"{msg}\n\n{original_content}")
|
|
216
|
+
|
|
217
|
+
click.secho(f"✅ Mensagem injetada com sucesso no editor!", fg="green")
|
|
218
|
+
except Exception as e:
|
|
219
|
+
click.secho(f"❌ Erro ao injetar no hook: {e}", fg="red")
|
|
220
|
+
else:
|
|
221
|
+
# MODO CONSOLE: O comportamento original que já existia
|
|
222
|
+
click.secho("\n💡 Dica: Use sem --commit para gerar o PR completo.\n", fg="yellow")
|
|
223
|
+
click.secho("\n📝 Sugestão de Commit:\n", fg="green", bold=True)
|
|
224
|
+
click.echo(msg)
|
|
225
|
+
click.echo("\n")
|
|
226
|
+
return
|
|
227
|
+
|
|
228
|
+
# Code Review e File Review (Arquivo)
|
|
229
|
+
if action_type in ["review", "fullreview", "filereview"]:
|
|
230
|
+
|
|
231
|
+
if fullreview:
|
|
232
|
+
pattern = os.getenv("OUTPUT_FILE_NAME_FULLREVIEW", "{branch}_{datetime}_PR_FULLREVIEW.txt")
|
|
233
|
+
elif action_type == "filereview":
|
|
234
|
+
pattern = os.getenv("OUTPUT_FILE_NAME_FILEREVIEW", "{branch}_{datetime}_FILE_REVIEW.txt")
|
|
235
|
+
else:
|
|
236
|
+
pattern = os.getenv("OUTPUT_FILE_NAME_REVIEW", "{branch}_{datetime}_PR_REVIEW.txt")
|
|
237
|
+
|
|
238
|
+
output_filename = pattern.format(
|
|
239
|
+
branch=safe_branch_name,
|
|
240
|
+
datetime=current_time
|
|
241
|
+
)
|
|
242
|
+
content = data.get('review', 'Nenhuma análise gerada.')
|
|
243
|
+
|
|
244
|
+
# Chama o Linter. Se for "filereview", ativa o modo de arquivo completo.
|
|
245
|
+
if action_type == "filereview":
|
|
246
|
+
linter_results = parse_diff_and_lint(diff_text, is_full_file=True, file_path=input)
|
|
247
|
+
else:
|
|
248
|
+
linter_results = parse_diff_and_lint(diff_text)
|
|
249
|
+
|
|
250
|
+
all_alerts = linter_results["errors"] + linter_results["warnings"]
|
|
251
|
+
|
|
252
|
+
if all_alerts:
|
|
253
|
+
|
|
254
|
+
click.secho(f"⚠️ Atenção! Encontrados {len(all_alerts)} alertas nas regras do Linter.", fg="yellow")
|
|
255
|
+
|
|
256
|
+
# Monta o cabeçalho com os erros do linter
|
|
257
|
+
linter_header = "## 🚨 Alertas de Análise Estática Local (Regras YAML)\n\n"
|
|
258
|
+
for alert in all_alerts:
|
|
259
|
+
linter_header += f"- {alert}\n"
|
|
260
|
+
linter_header += "\n---\n\n## 🤖 Code Review da IA\n\n"
|
|
261
|
+
|
|
262
|
+
# Injeta o cabeçalho no topo do conteúdo gerado pela IA
|
|
263
|
+
content = linter_header + content
|
|
264
|
+
else:
|
|
265
|
+
click.secho("✅ Linter Local passou sem violações de regras!", fg="green")
|
|
266
|
+
|
|
267
|
+
try:
|
|
268
|
+
with open(output_filename, "w", encoding="utf-8") as f:
|
|
269
|
+
f.write(content)
|
|
270
|
+
click.secho(f"\n✅ Code Review gerado com sucesso: '{output_filename}'", fg="green", bold=True)
|
|
271
|
+
except Exception as e:
|
|
272
|
+
click.secho(f"\n❌ Erro ao salvar o review: {e}", fg="red")
|
|
273
|
+
return
|
|
274
|
+
|
|
275
|
+
# Pull Request Padrão (Arquivo .md)
|
|
276
|
+
filename_pattern = os.getenv("OUTPUT_FILE_NAME", "{branch}_{datetime}_PR_DESC.md")
|
|
277
|
+
output_filename = filename_pattern.format(branch=safe_branch_name, datetime=current_time)
|
|
278
|
+
|
|
279
|
+
markdown_content = f"""# 🚀 Sugestão de Pull Request
|
|
280
|
+
|
|
281
|
+
**Commit Message Recomendada:**
|
|
282
|
+
```text
|
|
283
|
+
{data.get('commit_message', 'Atualização de código')}
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
---
|
|
287
|
+
|
|
288
|
+
{data.get('pr_description', 'Sem descrição detalhada.')}
|
|
289
|
+
"""
|
|
290
|
+
|
|
291
|
+
try:
|
|
292
|
+
with open(output_filename, "w", encoding="utf-8") as f:
|
|
293
|
+
f.write(markdown_content)
|
|
294
|
+
click.secho(f"\n✅ Sucesso! O arquivo '{output_filename}' foi gerado na pasta atual.", fg="green", bold=True)
|
|
295
|
+
except Exception as e:
|
|
296
|
+
click.secho(f"\n❌ Erro ao guardar o arquivo: {e}", fg="red")
|
|
297
|
+
|
|
298
|
+
if __name__ == "__main__":
|
|
299
|
+
cli()
|
security.py
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
from cryptography.fernet import Fernet
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
# Caminho onde a chave mestra de criptografia será guardada
|
|
5
|
+
KEY_PATH = Path.home() / ".gitpr" / "secret.key"
|
|
6
|
+
|
|
7
|
+
def get_or_create_key():
|
|
8
|
+
"""
|
|
9
|
+
Recupera a chave mestra do disco ou gera uma nova caso não exista.
|
|
10
|
+
"""
|
|
11
|
+
if not KEY_PATH.exists():
|
|
12
|
+
KEY_PATH.parent.mkdir(parents=True, exist_ok=True)
|
|
13
|
+
key = Fernet.generate_key()
|
|
14
|
+
with open(KEY_PATH, "wb") as key_file:
|
|
15
|
+
key_file.write(key)
|
|
16
|
+
return open(KEY_PATH, "rb").read()
|
|
17
|
+
|
|
18
|
+
def encrypt_data(data: str) -> str:
|
|
19
|
+
"""
|
|
20
|
+
Transforma uma string em um hash criptografado.
|
|
21
|
+
"""
|
|
22
|
+
if not data:
|
|
23
|
+
return ""
|
|
24
|
+
key = get_or_create_key()
|
|
25
|
+
f = Fernet(key)
|
|
26
|
+
# Encripta a string (convertida em bytes) e retorna como string legível
|
|
27
|
+
return f.encrypt(data.encode()).decode()
|
|
28
|
+
|
|
29
|
+
def decrypt_data(encrypted_data: str) -> str:
|
|
30
|
+
"""
|
|
31
|
+
Transforma o hash criptografado de volta na string original.
|
|
32
|
+
"""
|
|
33
|
+
if not encrypted_data:
|
|
34
|
+
return ""
|
|
35
|
+
try:
|
|
36
|
+
key = get_or_create_key()
|
|
37
|
+
f = Fernet(key)
|
|
38
|
+
# Desencripta e converte de volta para string (utf-8)
|
|
39
|
+
return f.decrypt(encrypted_data.encode()).decode()
|
|
40
|
+
except Exception:
|
|
41
|
+
# Caso a chave seja inválida ou os dados estejam corrompidos
|
|
42
|
+
return ""
|
updater.py
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
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.11"
|
|
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)
|