gitpr-cli 0.0.13__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gitpr-cli
3
- Version: 0.0.13
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.13
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:
@@ -9,6 +9,7 @@ gitpr_cli.egg-info/requires.txt
9
9
  gitpr_cli.egg-info/top_level.txt
10
10
  src/__init__.py
11
11
  src/ai_providers.py
12
+ src/blame_engine.py
12
13
  src/cache.py
13
14
  src/config.py
14
15
  src/core.py
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "gitpr-cli"
7
- version = "0.0.13"
7
+ version = "0.0.14"
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"
@@ -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():
@@ -188,6 +188,7 @@ def generate_skill_template():
188
188
  ".gitpr.review.md": "gitpr.review.md",
189
189
  ".gitpr.linter.yml": "gitpr.linter.yml",
190
190
  ".gitpr.filereview.md": "gitpr.filereview.md",
191
+ ".gitpr.blame.md": "gitpr.blame.md",
191
192
  }
192
193
 
193
194
  success_count = 0
@@ -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
 
@@ -137,6 +138,45 @@ def cli(commit, review, fullreview, linter, skill, update, installhooks, hook, q
137
138
  click.echo("---\n")
138
139
  return
139
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
+
140
180
  # Validação do Modo Input
141
181
  if input and not (review or fullreview):
142
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)
@@ -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.13"
10
+ __version__ = "0.0.14"
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