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.
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)