azure-pr-manager 1.0.0
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.
- package/package.json +48 -0
- package/src/api.js +274 -0
- package/src/config.js +78 -0
- package/src/git.js +174 -0
- package/src/index.js +860 -0
package/src/index.js
ADDED
|
@@ -0,0 +1,860 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { Command } from "commander";
|
|
4
|
+
import chalk from "chalk";
|
|
5
|
+
import inquirer from "inquirer";
|
|
6
|
+
import ora from "ora";
|
|
7
|
+
import { AzureDevOpsApi } from "./api.js";
|
|
8
|
+
import { loadConfig, saveConfig, getDefaultConfig } from "./config.js";
|
|
9
|
+
import * as git from "./git.js";
|
|
10
|
+
|
|
11
|
+
const program = new Command();
|
|
12
|
+
|
|
13
|
+
program
|
|
14
|
+
.name("azure-pr")
|
|
15
|
+
.description("CLI para criar Pull Requests no Azure DevOps automaticamente")
|
|
16
|
+
.version("1.0.0");
|
|
17
|
+
|
|
18
|
+
// ==================== INIT ====================
|
|
19
|
+
program
|
|
20
|
+
.command("init")
|
|
21
|
+
.description("Inicializa a configuracao do projeto (.prmanager.json)")
|
|
22
|
+
.action(async () => {
|
|
23
|
+
console.log(chalk.cyan("\n Azure PR Manager - Configuracao\n"));
|
|
24
|
+
|
|
25
|
+
const existing = await loadConfig();
|
|
26
|
+
const defaults = existing || getDefaultConfig();
|
|
27
|
+
|
|
28
|
+
// ── Organization ──
|
|
29
|
+
console.log(chalk.gray(" ┌─────────────────────────────────────────────────────"));
|
|
30
|
+
console.log(chalk.gray(" │") + chalk.white(" Como encontrar a Organization:"));
|
|
31
|
+
console.log(chalk.gray(" │") + " A organization esta na URL do seu Azure DevOps:");
|
|
32
|
+
console.log(chalk.gray(" │") + chalk.cyan(" https://dev.azure.com/") + chalk.yellow("SUA-ORGANIZATION") + chalk.cyan("/..."));
|
|
33
|
+
console.log(chalk.gray(" │") + " Tambem aparece no canto superior esquerdo ao logar.");
|
|
34
|
+
console.log(chalk.gray(" └─────────────────────────────────────────────────────\n"));
|
|
35
|
+
|
|
36
|
+
const { organization } = await inquirer.prompt([
|
|
37
|
+
{
|
|
38
|
+
type: "input",
|
|
39
|
+
name: "organization",
|
|
40
|
+
message: "Organization:",
|
|
41
|
+
default: defaults.organization || undefined,
|
|
42
|
+
validate: (v) => (v.trim() ? true : "Obrigatorio"),
|
|
43
|
+
},
|
|
44
|
+
]);
|
|
45
|
+
|
|
46
|
+
// ── Project ──
|
|
47
|
+
console.log(chalk.gray("\n ┌─────────────────────────────────────────────────────"));
|
|
48
|
+
console.log(chalk.gray(" │") + chalk.white(" Como encontrar o Project:"));
|
|
49
|
+
console.log(chalk.gray(" │") + " O project e o segundo trecho da URL, logo apos a org:");
|
|
50
|
+
console.log(chalk.gray(" │") + chalk.cyan(" https://dev.azure.com/" + organization + "/") + chalk.yellow("SEU-PROJETO") + chalk.cyan("/..."));
|
|
51
|
+
console.log(chalk.gray(" │") + " Na home da org, voce ve a lista de projetos disponiveis.");
|
|
52
|
+
console.log(chalk.gray(" └─────────────────────────────────────────────────────\n"));
|
|
53
|
+
|
|
54
|
+
const { project } = await inquirer.prompt([
|
|
55
|
+
{
|
|
56
|
+
type: "input",
|
|
57
|
+
name: "project",
|
|
58
|
+
message: "Project:",
|
|
59
|
+
default: defaults.project || undefined,
|
|
60
|
+
validate: (v) => (v.trim() ? true : "Obrigatorio"),
|
|
61
|
+
},
|
|
62
|
+
]);
|
|
63
|
+
|
|
64
|
+
// ── Token ──
|
|
65
|
+
console.log(chalk.gray("\n ┌─────────────────────────────────────────────────────"));
|
|
66
|
+
console.log(chalk.gray(" │") + chalk.white(" Como criar o Personal Access Token (PAT):"));
|
|
67
|
+
console.log(chalk.gray(" │"));
|
|
68
|
+
console.log(chalk.gray(" │") + " 1. Acesse " + chalk.cyan("dev.azure.com") + " e faca login");
|
|
69
|
+
console.log(chalk.gray(" │") + " 2. Clique no seu avatar (canto superior direito)");
|
|
70
|
+
console.log(chalk.gray(" │") + " 3. Selecione " + chalk.white("Personal access tokens"));
|
|
71
|
+
console.log(chalk.gray(" │") + " 4. Clique em " + chalk.white("+ New Token"));
|
|
72
|
+
console.log(chalk.gray(" │") + " 5. De um nome, ex: " + chalk.yellow("\"PR Manager CLI\""));
|
|
73
|
+
console.log(chalk.gray(" │") + " 6. Selecione a org: " + chalk.yellow(organization));
|
|
74
|
+
console.log(chalk.gray(" │") + " 7. Em Scopes, marque:");
|
|
75
|
+
console.log(chalk.gray(" │") + chalk.green(" ✓ Code > Read & Write"));
|
|
76
|
+
console.log(chalk.gray(" │") + chalk.green(" ✓ Identity > Read"));
|
|
77
|
+
console.log(chalk.gray(" │") + chalk.green(" ✓ Member Entitlement Management > Read"));
|
|
78
|
+
console.log(chalk.gray(" │") + " 8. Clique em Create e copie o token gerado");
|
|
79
|
+
console.log(chalk.gray(" │"));
|
|
80
|
+
console.log(chalk.gray(" │") + chalk.red(" O token so aparece uma vez! Salve em lugar seguro."));
|
|
81
|
+
console.log(chalk.gray(" └─────────────────────────────────────────────────────\n"));
|
|
82
|
+
|
|
83
|
+
const { token } = await inquirer.prompt([
|
|
84
|
+
{
|
|
85
|
+
type: "password",
|
|
86
|
+
name: "token",
|
|
87
|
+
message: "Personal Access Token (PAT):",
|
|
88
|
+
mask: "*",
|
|
89
|
+
validate: (v) => (v.trim() ? true : "Obrigatorio"),
|
|
90
|
+
},
|
|
91
|
+
]);
|
|
92
|
+
|
|
93
|
+
// Testar conexao e listar repos
|
|
94
|
+
const spinner = ora("Testando conexao...").start();
|
|
95
|
+
const api = new AzureDevOpsApi({
|
|
96
|
+
organization: organization,
|
|
97
|
+
project: project,
|
|
98
|
+
repository: "",
|
|
99
|
+
token: token,
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
const test = await api.testConnection();
|
|
103
|
+
if (!test.success) {
|
|
104
|
+
spinner.fail("Falha na conexao: " + test.error);
|
|
105
|
+
process.exit(1);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
spinner.succeed(`Conectado! ${test.repos} repositorio(s) encontrado(s)`);
|
|
109
|
+
|
|
110
|
+
// Selecionar repositorio
|
|
111
|
+
console.log(chalk.gray("\n ┌─────────────────────────────────────────────────────"));
|
|
112
|
+
console.log(chalk.gray(" │") + chalk.white(" Selecione o repositorio:"));
|
|
113
|
+
console.log(chalk.gray(" │") + " Estes sao os repos Git encontrados no projeto");
|
|
114
|
+
console.log(chalk.gray(" │") + chalk.cyan(` ${project}`) + ". Escolha o que voce usa para criar PRs.");
|
|
115
|
+
console.log(chalk.gray(" └─────────────────────────────────────────────────────\n"));
|
|
116
|
+
|
|
117
|
+
const repos = await api.getRepositories();
|
|
118
|
+
const { repository } = await inquirer.prompt([
|
|
119
|
+
{
|
|
120
|
+
type: "list",
|
|
121
|
+
name: "repository",
|
|
122
|
+
message: "Selecione o repositorio:",
|
|
123
|
+
choices: repos.map((r) => ({
|
|
124
|
+
name: `${r.name} (default: ${r.defaultBranch || "N/A"})`,
|
|
125
|
+
value: r.name,
|
|
126
|
+
})),
|
|
127
|
+
},
|
|
128
|
+
]);
|
|
129
|
+
|
|
130
|
+
// Formato do titulo
|
|
131
|
+
console.log(chalk.gray("\n ┌─────────────────────────────────────────────────────"));
|
|
132
|
+
console.log(chalk.gray(" │") + chalk.white(" Formato do titulo do PR:"));
|
|
133
|
+
console.log(chalk.gray(" │") + " O titulo sera gerado automaticamente com base nas");
|
|
134
|
+
console.log(chalk.gray(" │") + " branches. Use variaveis:");
|
|
135
|
+
console.log(chalk.gray(" │") + chalk.cyan(" {source}") + " = branch de origem (ex: feature/login)");
|
|
136
|
+
console.log(chalk.gray(" │") + chalk.cyan(" {target}") + " = branch de destino (ex: develop)");
|
|
137
|
+
console.log(chalk.gray(" │") + " Resultado: " + chalk.yellow("feature/login -> develop"));
|
|
138
|
+
console.log(chalk.gray(" └─────────────────────────────────────────────────────\n"));
|
|
139
|
+
|
|
140
|
+
const { titleFormat } = await inquirer.prompt([
|
|
141
|
+
{
|
|
142
|
+
type: "list",
|
|
143
|
+
name: "titleFormat",
|
|
144
|
+
message: "Formato do titulo do PR:",
|
|
145
|
+
choices: [
|
|
146
|
+
{ name: "{source} -> {target}", value: "{source} -> {target}" },
|
|
147
|
+
{
|
|
148
|
+
name: "{source} => {target}",
|
|
149
|
+
value: "{source} => {target}",
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
name: "[{source}] {target}",
|
|
153
|
+
value: "[{source}] {target}",
|
|
154
|
+
},
|
|
155
|
+
{ name: "Personalizado", value: "_custom" },
|
|
156
|
+
],
|
|
157
|
+
default: defaults.titleFormat,
|
|
158
|
+
},
|
|
159
|
+
]);
|
|
160
|
+
|
|
161
|
+
let finalTitleFormat = titleFormat;
|
|
162
|
+
if (titleFormat === "_custom") {
|
|
163
|
+
const { custom } = await inquirer.prompt([
|
|
164
|
+
{
|
|
165
|
+
type: "input",
|
|
166
|
+
name: "custom",
|
|
167
|
+
message: "Formato personalizado (use {source} e {target}):",
|
|
168
|
+
default: "{source} -> {target}",
|
|
169
|
+
},
|
|
170
|
+
]);
|
|
171
|
+
finalTitleFormat = custom;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Reviewers
|
|
175
|
+
console.log(chalk.gray("\n ┌─────────────────────────────────────────────────────"));
|
|
176
|
+
console.log(chalk.gray(" │") + chalk.white(" Required Reviewers:"));
|
|
177
|
+
console.log(chalk.gray(" │") + " Pessoas que PRECISAM aprovar o PR para ele ser completado.");
|
|
178
|
+
console.log(chalk.gray(" │") + " Use o email corporativo de cada pessoa (o mesmo que usam");
|
|
179
|
+
console.log(chalk.gray(" │") + " pra logar no Azure DevOps). Separe multiplos por virgula.");
|
|
180
|
+
console.log(chalk.gray(" │") + chalk.yellow(" Ex: joao@empresa.com, maria@empresa.com"));
|
|
181
|
+
console.log(chalk.gray(" │") + " Deixe vazio se nao quiser required reviewers.");
|
|
182
|
+
console.log(chalk.gray(" └─────────────────────────────────────────────────────\n"));
|
|
183
|
+
|
|
184
|
+
const { requiredEmails } = await inquirer.prompt([
|
|
185
|
+
{
|
|
186
|
+
type: "input",
|
|
187
|
+
name: "requiredEmails",
|
|
188
|
+
message:
|
|
189
|
+
"Required reviewers (emails separados por virgula, ou vazio):",
|
|
190
|
+
default: (defaults.requiredReviewers || []).join(", "),
|
|
191
|
+
},
|
|
192
|
+
]);
|
|
193
|
+
|
|
194
|
+
console.log(chalk.gray("\n ┌─────────────────────────────────────────────────────"));
|
|
195
|
+
console.log(chalk.gray(" │") + chalk.white(" Optional Reviewers:"));
|
|
196
|
+
console.log(chalk.gray(" │") + " Pessoas que serao notificadas mas NAO precisam aprovar.");
|
|
197
|
+
console.log(chalk.gray(" │") + " Mesmo formato: emails separados por virgula.");
|
|
198
|
+
console.log(chalk.gray(" └─────────────────────────────────────────────────────\n"));
|
|
199
|
+
|
|
200
|
+
const { optionalEmails } = await inquirer.prompt([
|
|
201
|
+
{
|
|
202
|
+
type: "input",
|
|
203
|
+
name: "optionalEmails",
|
|
204
|
+
message:
|
|
205
|
+
"Optional reviewers (emails separados por virgula, ou vazio):",
|
|
206
|
+
default: (defaults.optionalReviewers || []).join(", "),
|
|
207
|
+
},
|
|
208
|
+
]);
|
|
209
|
+
|
|
210
|
+
// Defaults
|
|
211
|
+
console.log(chalk.gray("\n ┌─────────────────────────────────────────────────────"));
|
|
212
|
+
console.log(chalk.gray(" │") + chalk.white(" Opcoes padrao para novos PRs:"));
|
|
213
|
+
console.log(chalk.gray(" │") + chalk.cyan(" Draft") + " = PR criado como rascunho (nao pode ser completado");
|
|
214
|
+
console.log(chalk.gray(" │") + " ate voce publicar).");
|
|
215
|
+
console.log(chalk.gray(" │") + chalk.cyan(" Auto-complete") + " = quando todos aprovarem e policies passarem,");
|
|
216
|
+
console.log(chalk.gray(" │") + " o PR e completado automaticamente (squash + delete branch).");
|
|
217
|
+
console.log(chalk.gray(" └─────────────────────────────────────────────────────\n"));
|
|
218
|
+
|
|
219
|
+
const defaultOpts = await inquirer.prompt([
|
|
220
|
+
{
|
|
221
|
+
type: "confirm",
|
|
222
|
+
name: "isDraft",
|
|
223
|
+
message: "Criar PRs como draft por padrao?",
|
|
224
|
+
default: defaults.defaults?.isDraft || false,
|
|
225
|
+
},
|
|
226
|
+
{
|
|
227
|
+
type: "confirm",
|
|
228
|
+
name: "autoComplete",
|
|
229
|
+
message: "Ativar auto-complete por padrao?",
|
|
230
|
+
default: defaults.defaults?.autoComplete || false,
|
|
231
|
+
},
|
|
232
|
+
]);
|
|
233
|
+
|
|
234
|
+
const config = {
|
|
235
|
+
organization: organization,
|
|
236
|
+
project: project,
|
|
237
|
+
repository,
|
|
238
|
+
token: token,
|
|
239
|
+
titleFormat: finalTitleFormat,
|
|
240
|
+
requiredReviewers: requiredEmails
|
|
241
|
+
.split(",")
|
|
242
|
+
.map((e) => e.trim())
|
|
243
|
+
.filter(Boolean),
|
|
244
|
+
optionalReviewers: optionalEmails
|
|
245
|
+
.split(",")
|
|
246
|
+
.map((e) => e.trim())
|
|
247
|
+
.filter(Boolean),
|
|
248
|
+
defaults: {
|
|
249
|
+
isDraft: defaultOpts.isDraft,
|
|
250
|
+
autoComplete: defaultOpts.autoComplete,
|
|
251
|
+
deleteSourceBranch: true,
|
|
252
|
+
},
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
const configPath = await saveConfig(config);
|
|
256
|
+
console.log(
|
|
257
|
+
chalk.green(`\n Configuracao salva em: ${configPath}`)
|
|
258
|
+
);
|
|
259
|
+
console.log(
|
|
260
|
+
chalk.yellow(
|
|
261
|
+
" Adicione .prmanager.json ao .gitignore (contem seu token)!\n"
|
|
262
|
+
)
|
|
263
|
+
);
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
// ==================== CREATE ====================
|
|
267
|
+
program
|
|
268
|
+
.command("create")
|
|
269
|
+
.description("Cria um Pull Request (com fluxo git completo)")
|
|
270
|
+
.option("-t, --target <branch>", "Branch de destino")
|
|
271
|
+
.option("--title <title>", "Titulo customizado")
|
|
272
|
+
.option("--draft", "Criar como draft")
|
|
273
|
+
.option("--auto-complete", "Ativar auto-complete")
|
|
274
|
+
.option("--skip-git", "Pular verificacoes git (ir direto para criacao da PR)")
|
|
275
|
+
.action(async (opts) => {
|
|
276
|
+
const config = await loadConfig();
|
|
277
|
+
if (!config) {
|
|
278
|
+
console.log(
|
|
279
|
+
chalk.red(
|
|
280
|
+
"\n Configuracao nao encontrada. Execute: azure-pr init\n"
|
|
281
|
+
)
|
|
282
|
+
);
|
|
283
|
+
process.exit(1);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
console.log(chalk.cyan("\n Azure PR Manager - Criar Pull Request\n"));
|
|
287
|
+
|
|
288
|
+
// ── ETAPA 1: Verificar repositorio git ──
|
|
289
|
+
if (!git.isGitRepo()) {
|
|
290
|
+
console.log(
|
|
291
|
+
chalk.red(
|
|
292
|
+
" Voce nao esta dentro de um repositorio git.\n" +
|
|
293
|
+
" Execute este comando na raiz do seu projeto.\n"
|
|
294
|
+
)
|
|
295
|
+
);
|
|
296
|
+
process.exit(1);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const currentBranch = git.getCurrentBranch();
|
|
300
|
+
if (!currentBranch || currentBranch === "HEAD") {
|
|
301
|
+
console.log(
|
|
302
|
+
chalk.red(
|
|
303
|
+
" Nao foi possivel detectar a branch atual (detached HEAD?).\n" +
|
|
304
|
+
" Faca checkout de uma branch antes de criar o PR.\n"
|
|
305
|
+
)
|
|
306
|
+
);
|
|
307
|
+
process.exit(1);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
console.log(
|
|
311
|
+
chalk.white(" Branch atual: ") + chalk.cyan(currentBranch)
|
|
312
|
+
);
|
|
313
|
+
|
|
314
|
+
const sourceBranch = currentBranch;
|
|
315
|
+
|
|
316
|
+
if (!opts.skipGit) {
|
|
317
|
+
// ── ETAPA 2: Verificar alteracoes pendentes ──
|
|
318
|
+
const status = git.getWorkingTreeStatus();
|
|
319
|
+
|
|
320
|
+
if (!status.clean) {
|
|
321
|
+
console.log();
|
|
322
|
+
console.log(
|
|
323
|
+
chalk.yellow(" Existem alteracoes pendentes no seu repositorio:")
|
|
324
|
+
);
|
|
325
|
+
|
|
326
|
+
if (status.stagedFiles.length > 0) {
|
|
327
|
+
console.log(
|
|
328
|
+
chalk.green(` Staged: ${status.stagedFiles.length} arquivo(s)`)
|
|
329
|
+
);
|
|
330
|
+
status.stagedFiles.forEach((f) =>
|
|
331
|
+
console.log(chalk.green(` + ${f}`))
|
|
332
|
+
);
|
|
333
|
+
}
|
|
334
|
+
if (status.modifiedFiles.length > 0) {
|
|
335
|
+
console.log(
|
|
336
|
+
chalk.red(
|
|
337
|
+
` Modificados: ${status.modifiedFiles.length} arquivo(s)`
|
|
338
|
+
)
|
|
339
|
+
);
|
|
340
|
+
status.modifiedFiles.forEach((f) =>
|
|
341
|
+
console.log(chalk.red(` ~ ${f}`))
|
|
342
|
+
);
|
|
343
|
+
}
|
|
344
|
+
if (status.untrackedFiles.length > 0) {
|
|
345
|
+
console.log(
|
|
346
|
+
chalk.gray(
|
|
347
|
+
` Nao rastreados: ${status.untrackedFiles.length} arquivo(s)`
|
|
348
|
+
)
|
|
349
|
+
);
|
|
350
|
+
status.untrackedFiles.forEach((f) =>
|
|
351
|
+
console.log(chalk.gray(` ? ${f}`))
|
|
352
|
+
);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
console.log();
|
|
356
|
+
|
|
357
|
+
const { action } = await inquirer.prompt([
|
|
358
|
+
{
|
|
359
|
+
type: "list",
|
|
360
|
+
name: "action",
|
|
361
|
+
message: "O que deseja fazer com as alteracoes pendentes?",
|
|
362
|
+
choices: [
|
|
363
|
+
{
|
|
364
|
+
name: "Adicionar tudo e fazer commit (git add -A && git commit)",
|
|
365
|
+
value: "commit",
|
|
366
|
+
},
|
|
367
|
+
{
|
|
368
|
+
name: "Continuar sem commitar (alteracoes ficam pendentes)",
|
|
369
|
+
value: "skip",
|
|
370
|
+
},
|
|
371
|
+
{
|
|
372
|
+
name: "Cancelar",
|
|
373
|
+
value: "cancel",
|
|
374
|
+
},
|
|
375
|
+
],
|
|
376
|
+
},
|
|
377
|
+
]);
|
|
378
|
+
|
|
379
|
+
if (action === "cancel") {
|
|
380
|
+
console.log(chalk.yellow("\n Cancelado.\n"));
|
|
381
|
+
process.exit(0);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
if (action === "commit") {
|
|
385
|
+
console.log(
|
|
386
|
+
chalk.gray(
|
|
387
|
+
"\n ┌─────────────────────────────────────────────────────"
|
|
388
|
+
)
|
|
389
|
+
);
|
|
390
|
+
console.log(
|
|
391
|
+
chalk.gray(" │") +
|
|
392
|
+
chalk.white(" Mensagem de commit:")
|
|
393
|
+
);
|
|
394
|
+
console.log(
|
|
395
|
+
chalk.gray(" │") +
|
|
396
|
+
" Escreva uma mensagem descritiva sobre o que foi alterado."
|
|
397
|
+
);
|
|
398
|
+
console.log(
|
|
399
|
+
chalk.gray(" │") +
|
|
400
|
+
" Boas praticas: comece com verbo no imperativo."
|
|
401
|
+
);
|
|
402
|
+
console.log(
|
|
403
|
+
chalk.gray(" │") +
|
|
404
|
+
chalk.yellow(' Ex: "Adiciona validacao no formulario de login"')
|
|
405
|
+
);
|
|
406
|
+
console.log(
|
|
407
|
+
chalk.gray(
|
|
408
|
+
" └─────────────────────────────────────────────────────\n"
|
|
409
|
+
)
|
|
410
|
+
);
|
|
411
|
+
|
|
412
|
+
const { commitMessage } = await inquirer.prompt([
|
|
413
|
+
{
|
|
414
|
+
type: "input",
|
|
415
|
+
name: "commitMessage",
|
|
416
|
+
message: "Mensagem do commit:",
|
|
417
|
+
validate: (v) =>
|
|
418
|
+
v.trim() ? true : "A mensagem de commit e obrigatoria",
|
|
419
|
+
},
|
|
420
|
+
]);
|
|
421
|
+
|
|
422
|
+
const commitSpinner = ora("Commitando alteracoes...").start();
|
|
423
|
+
try {
|
|
424
|
+
git.stageAll();
|
|
425
|
+
git.commit(commitMessage);
|
|
426
|
+
commitSpinner.succeed(
|
|
427
|
+
`Commit realizado: "${commitMessage}"`
|
|
428
|
+
);
|
|
429
|
+
} catch (err) {
|
|
430
|
+
commitSpinner.fail("Erro ao commitar: " + err.message);
|
|
431
|
+
process.exit(1);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
} else {
|
|
435
|
+
console.log(chalk.green(" Working tree limpa - nenhuma alteracao pendente"));
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// ── ETAPA 3: Verificar remote e push ──
|
|
439
|
+
if (!git.hasRemoteOrigin()) {
|
|
440
|
+
console.log(
|
|
441
|
+
chalk.red(
|
|
442
|
+
"\n Nenhum remote 'origin' configurado.\n" +
|
|
443
|
+
' Configure com: git remote add origin <url>\n'
|
|
444
|
+
)
|
|
445
|
+
);
|
|
446
|
+
process.exit(1);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
const fetchSpinner = ora("Sincronizando com remote...").start();
|
|
450
|
+
git.fetchOrigin();
|
|
451
|
+
fetchSpinner.succeed("Refs atualizadas");
|
|
452
|
+
|
|
453
|
+
const existsOnRemote = git.branchExistsOnRemote(currentBranch);
|
|
454
|
+
|
|
455
|
+
if (!existsOnRemote) {
|
|
456
|
+
// Branch nao existe no remote - precisa publicar
|
|
457
|
+
console.log();
|
|
458
|
+
console.log(
|
|
459
|
+
chalk.yellow(
|
|
460
|
+
` A branch "${currentBranch}" ainda nao existe no remote.`
|
|
461
|
+
)
|
|
462
|
+
);
|
|
463
|
+
|
|
464
|
+
const { shouldPublish } = await inquirer.prompt([
|
|
465
|
+
{
|
|
466
|
+
type: "confirm",
|
|
467
|
+
name: "shouldPublish",
|
|
468
|
+
message: `Publicar branch "${currentBranch}" no remote (git push -u origin ${currentBranch})?`,
|
|
469
|
+
default: true,
|
|
470
|
+
},
|
|
471
|
+
]);
|
|
472
|
+
|
|
473
|
+
if (!shouldPublish) {
|
|
474
|
+
console.log(
|
|
475
|
+
chalk.yellow(
|
|
476
|
+
"\n Cancelado. A branch precisa estar no remote para criar o PR.\n"
|
|
477
|
+
)
|
|
478
|
+
);
|
|
479
|
+
process.exit(0);
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
const pushSpinner = ora(
|
|
483
|
+
`Publicando branch "${currentBranch}"...`
|
|
484
|
+
).start();
|
|
485
|
+
try {
|
|
486
|
+
git.publishBranch(currentBranch);
|
|
487
|
+
pushSpinner.succeed(
|
|
488
|
+
`Branch "${currentBranch}" publicada no remote`
|
|
489
|
+
);
|
|
490
|
+
} catch (err) {
|
|
491
|
+
pushSpinner.fail("Erro ao publicar branch: " + err.message);
|
|
492
|
+
process.exit(1);
|
|
493
|
+
}
|
|
494
|
+
} else {
|
|
495
|
+
// Branch existe no remote - verificar commits pendentes
|
|
496
|
+
const unpushedCount = git.getUnpushedCommitCount(currentBranch);
|
|
497
|
+
|
|
498
|
+
if (unpushedCount > 0) {
|
|
499
|
+
const unpushedCommits = git.getUnpushedCommits(currentBranch);
|
|
500
|
+
console.log();
|
|
501
|
+
console.log(
|
|
502
|
+
chalk.yellow(
|
|
503
|
+
` ${unpushedCount} commit(s) pendente(s) para push:`
|
|
504
|
+
)
|
|
505
|
+
);
|
|
506
|
+
unpushedCommits.forEach((c) =>
|
|
507
|
+
console.log(chalk.gray(` ${c}`))
|
|
508
|
+
);
|
|
509
|
+
|
|
510
|
+
const { shouldPush } = await inquirer.prompt([
|
|
511
|
+
{
|
|
512
|
+
type: "confirm",
|
|
513
|
+
name: "shouldPush",
|
|
514
|
+
message: "Enviar commits para o remote (git push)?",
|
|
515
|
+
default: true,
|
|
516
|
+
},
|
|
517
|
+
]);
|
|
518
|
+
|
|
519
|
+
if (shouldPush) {
|
|
520
|
+
const pushSpinner = ora("Enviando commits...").start();
|
|
521
|
+
try {
|
|
522
|
+
git.push();
|
|
523
|
+
pushSpinner.succeed(
|
|
524
|
+
`${unpushedCount} commit(s) enviado(s) ao remote`
|
|
525
|
+
);
|
|
526
|
+
} catch (err) {
|
|
527
|
+
pushSpinner.fail("Erro ao enviar commits: " + err.message);
|
|
528
|
+
process.exit(1);
|
|
529
|
+
}
|
|
530
|
+
} else {
|
|
531
|
+
console.log(
|
|
532
|
+
chalk.yellow(
|
|
533
|
+
" Continuando sem push. O PR sera criado com base no que ja esta no remote.\n"
|
|
534
|
+
)
|
|
535
|
+
);
|
|
536
|
+
}
|
|
537
|
+
} else {
|
|
538
|
+
console.log(
|
|
539
|
+
chalk.green(" Branch sincronizada com remote - nenhum commit pendente")
|
|
540
|
+
);
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
// ── ETAPA 4: Selecionar branch de destino e criar PR ──
|
|
546
|
+
console.log();
|
|
547
|
+
|
|
548
|
+
const api = new AzureDevOpsApi(config);
|
|
549
|
+
|
|
550
|
+
// Buscar branches do Azure DevOps
|
|
551
|
+
const branchSpinner = ora("Buscando branches do repositorio...").start();
|
|
552
|
+
let branches;
|
|
553
|
+
try {
|
|
554
|
+
branches = await api.getBranches();
|
|
555
|
+
branchSpinner.succeed(`${branches.length} branches encontradas`);
|
|
556
|
+
} catch (err) {
|
|
557
|
+
branchSpinner.fail("Erro ao buscar branches: " + err.message);
|
|
558
|
+
process.exit(1);
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
const branchNames = branches.map((b) => b.name);
|
|
562
|
+
|
|
563
|
+
// Verificar se a source branch existe no Azure DevOps
|
|
564
|
+
if (!branchNames.includes(sourceBranch)) {
|
|
565
|
+
console.log(
|
|
566
|
+
chalk.red(
|
|
567
|
+
`\n A branch "${sourceBranch}" nao foi encontrada no repositorio "${config.repository}" do Azure DevOps.\n` +
|
|
568
|
+
" Verifique se voce fez push da branch e se o repositorio esta correto.\n"
|
|
569
|
+
)
|
|
570
|
+
);
|
|
571
|
+
process.exit(1);
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
// Selecionar target
|
|
575
|
+
let targetBranch = opts.target;
|
|
576
|
+
if (!targetBranch) {
|
|
577
|
+
const targetChoices = branchNames.filter((b) => b !== sourceBranch);
|
|
578
|
+
|
|
579
|
+
console.log(
|
|
580
|
+
chalk.gray(
|
|
581
|
+
"\n ┌─────────────────────────────────────────────────────"
|
|
582
|
+
)
|
|
583
|
+
);
|
|
584
|
+
console.log(
|
|
585
|
+
chalk.gray(" │") + chalk.white(" Branch de destino (target):")
|
|
586
|
+
);
|
|
587
|
+
console.log(
|
|
588
|
+
chalk.gray(" │") +
|
|
589
|
+
" A branch para onde suas alteracoes serao mescladas."
|
|
590
|
+
);
|
|
591
|
+
console.log(
|
|
592
|
+
chalk.gray(" │") +
|
|
593
|
+
" Geralmente e " +
|
|
594
|
+
chalk.cyan("main") +
|
|
595
|
+
", " +
|
|
596
|
+
chalk.cyan("master") +
|
|
597
|
+
" ou " +
|
|
598
|
+
chalk.cyan("develop") +
|
|
599
|
+
"."
|
|
600
|
+
);
|
|
601
|
+
console.log(
|
|
602
|
+
chalk.gray(
|
|
603
|
+
" └─────────────────────────────────────────────────────\n"
|
|
604
|
+
)
|
|
605
|
+
);
|
|
606
|
+
|
|
607
|
+
const { target } = await inquirer.prompt([
|
|
608
|
+
{
|
|
609
|
+
type: "list",
|
|
610
|
+
name: "target",
|
|
611
|
+
message: "Branch de destino (target):",
|
|
612
|
+
choices: targetChoices,
|
|
613
|
+
default: targetChoices.includes("main")
|
|
614
|
+
? "main"
|
|
615
|
+
: targetChoices.includes("master")
|
|
616
|
+
? "master"
|
|
617
|
+
: targetChoices.includes("develop")
|
|
618
|
+
? "develop"
|
|
619
|
+
: undefined,
|
|
620
|
+
loop: false,
|
|
621
|
+
pageSize: 15,
|
|
622
|
+
},
|
|
623
|
+
]);
|
|
624
|
+
targetBranch = target;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
// Titulo
|
|
628
|
+
let title = opts.title;
|
|
629
|
+
if (!title) {
|
|
630
|
+
const autoTitle = config.titleFormat
|
|
631
|
+
.replace(/\{source\}/g, sourceBranch)
|
|
632
|
+
.replace(/\{target\}/g, targetBranch);
|
|
633
|
+
|
|
634
|
+
const { finalTitle } = await inquirer.prompt([
|
|
635
|
+
{
|
|
636
|
+
type: "input",
|
|
637
|
+
name: "finalTitle",
|
|
638
|
+
message: "Titulo do PR:",
|
|
639
|
+
default: autoTitle,
|
|
640
|
+
},
|
|
641
|
+
]);
|
|
642
|
+
title = finalTitle;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
// Descricao
|
|
646
|
+
console.log(
|
|
647
|
+
chalk.gray(
|
|
648
|
+
"\n ┌─────────────────────────────────────────────────────"
|
|
649
|
+
)
|
|
650
|
+
);
|
|
651
|
+
console.log(
|
|
652
|
+
chalk.gray(" │") + chalk.white(" Descricao do PR:")
|
|
653
|
+
);
|
|
654
|
+
console.log(
|
|
655
|
+
chalk.gray(" │") +
|
|
656
|
+
" Descreva o que foi feito neste PR. Pode usar Markdown."
|
|
657
|
+
);
|
|
658
|
+
console.log(
|
|
659
|
+
chalk.gray(" │") +
|
|
660
|
+
" Deixe vazio e pressione Enter para pular."
|
|
661
|
+
);
|
|
662
|
+
console.log(
|
|
663
|
+
chalk.gray(
|
|
664
|
+
" └─────────────────────────────────────────────────────\n"
|
|
665
|
+
)
|
|
666
|
+
);
|
|
667
|
+
|
|
668
|
+
const { description } = await inquirer.prompt([
|
|
669
|
+
{
|
|
670
|
+
type: "input",
|
|
671
|
+
name: "description",
|
|
672
|
+
message: "Descricao do PR (ou Enter para vazio):",
|
|
673
|
+
default: "",
|
|
674
|
+
},
|
|
675
|
+
]);
|
|
676
|
+
|
|
677
|
+
// Draft / Auto-complete
|
|
678
|
+
const isDraft =
|
|
679
|
+
opts.draft !== undefined
|
|
680
|
+
? opts.draft
|
|
681
|
+
: config.defaults?.isDraft || false;
|
|
682
|
+
const autoComplete =
|
|
683
|
+
opts.autoComplete !== undefined
|
|
684
|
+
? opts.autoComplete
|
|
685
|
+
: config.defaults?.autoComplete || false;
|
|
686
|
+
|
|
687
|
+
// Resolver reviewers
|
|
688
|
+
const resolvedRequired = [];
|
|
689
|
+
const resolvedOptional = [];
|
|
690
|
+
|
|
691
|
+
if (config.requiredReviewers?.length > 0) {
|
|
692
|
+
const s = ora("Resolvendo required reviewers...").start();
|
|
693
|
+
try {
|
|
694
|
+
const resolved = await api.resolveReviewers(
|
|
695
|
+
config.requiredReviewers
|
|
696
|
+
);
|
|
697
|
+
resolvedRequired.push(...resolved);
|
|
698
|
+
s.succeed(
|
|
699
|
+
`${resolved.length} required reviewer(s): ${resolved.map((r) => r.displayName).join(", ")}`
|
|
700
|
+
);
|
|
701
|
+
} catch (err) {
|
|
702
|
+
s.warn("Erro ao resolver required reviewers: " + err.message);
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
if (config.optionalReviewers?.length > 0) {
|
|
707
|
+
const s = ora("Resolvendo optional reviewers...").start();
|
|
708
|
+
try {
|
|
709
|
+
const resolved = await api.resolveReviewers(
|
|
710
|
+
config.optionalReviewers
|
|
711
|
+
);
|
|
712
|
+
resolvedOptional.push(...resolved);
|
|
713
|
+
s.succeed(
|
|
714
|
+
`${resolved.length} optional reviewer(s): ${resolved.map((r) => r.displayName).join(", ")}`
|
|
715
|
+
);
|
|
716
|
+
} catch (err) {
|
|
717
|
+
s.warn("Erro ao resolver optional reviewers: " + err.message);
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
// Confirmacao
|
|
722
|
+
console.log(chalk.cyan("\n ╔═══════════════════════════════════════════"));
|
|
723
|
+
console.log(chalk.cyan(" ║") + chalk.white(" Resumo do PR"));
|
|
724
|
+
console.log(chalk.cyan(" ╠═══════════════════════════════════════════"));
|
|
725
|
+
console.log(
|
|
726
|
+
chalk.cyan(" ║") +
|
|
727
|
+
` ${chalk.gray("Source:")} ${chalk.green(sourceBranch)}`
|
|
728
|
+
);
|
|
729
|
+
console.log(
|
|
730
|
+
chalk.cyan(" ║") +
|
|
731
|
+
` ${chalk.gray("Target:")} ${chalk.yellow(targetBranch)}`
|
|
732
|
+
);
|
|
733
|
+
console.log(
|
|
734
|
+
chalk.cyan(" ║") + ` ${chalk.gray("Titulo:")} ${title}`
|
|
735
|
+
);
|
|
736
|
+
if (description) {
|
|
737
|
+
console.log(
|
|
738
|
+
chalk.cyan(" ║") +
|
|
739
|
+
` ${chalk.gray("Descricao:")} ${description.substring(0, 50)}${description.length > 50 ? "..." : ""}`
|
|
740
|
+
);
|
|
741
|
+
}
|
|
742
|
+
console.log(
|
|
743
|
+
chalk.cyan(" ║") +
|
|
744
|
+
` ${chalk.gray("Draft:")} ${isDraft ? "Sim" : "Nao"}`
|
|
745
|
+
);
|
|
746
|
+
console.log(
|
|
747
|
+
chalk.cyan(" ║") +
|
|
748
|
+
` ${chalk.gray("Auto-complete:")} ${autoComplete ? "Sim" : "Nao"}`
|
|
749
|
+
);
|
|
750
|
+
if (resolvedRequired.length > 0) {
|
|
751
|
+
console.log(
|
|
752
|
+
chalk.cyan(" ║") +
|
|
753
|
+
` ${chalk.gray("Required:")} ${resolvedRequired.map((r) => r.displayName).join(", ")}`
|
|
754
|
+
);
|
|
755
|
+
}
|
|
756
|
+
if (resolvedOptional.length > 0) {
|
|
757
|
+
console.log(
|
|
758
|
+
chalk.cyan(" ║") +
|
|
759
|
+
` ${chalk.gray("Optional:")} ${resolvedOptional.map((r) => r.displayName).join(", ")}`
|
|
760
|
+
);
|
|
761
|
+
}
|
|
762
|
+
console.log(chalk.cyan(" ╚═══════════════════════════════════════════\n"));
|
|
763
|
+
|
|
764
|
+
const { confirm } = await inquirer.prompt([
|
|
765
|
+
{
|
|
766
|
+
type: "confirm",
|
|
767
|
+
name: "confirm",
|
|
768
|
+
message: "Criar este PR?",
|
|
769
|
+
default: true,
|
|
770
|
+
},
|
|
771
|
+
]);
|
|
772
|
+
|
|
773
|
+
if (!confirm) {
|
|
774
|
+
console.log(chalk.yellow("\n Cancelado.\n"));
|
|
775
|
+
process.exit(0);
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
// Criar PR
|
|
779
|
+
const createSpinner = ora("Criando Pull Request...").start();
|
|
780
|
+
try {
|
|
781
|
+
const pr = await api.createPullRequest({
|
|
782
|
+
sourceBranch,
|
|
783
|
+
targetBranch,
|
|
784
|
+
title,
|
|
785
|
+
description: description || "",
|
|
786
|
+
reviewers: resolvedRequired,
|
|
787
|
+
optionalReviewers: resolvedOptional,
|
|
788
|
+
isDraft,
|
|
789
|
+
autoComplete,
|
|
790
|
+
});
|
|
791
|
+
|
|
792
|
+
const prUrl = `https://dev.azure.com/${config.organization}/${config.project}/_git/${config.repository}/pullrequest/${pr.pullRequestId}`;
|
|
793
|
+
createSpinner.succeed("Pull Request criado com sucesso!");
|
|
794
|
+
console.log(chalk.green(`\n PR #${pr.pullRequestId}: ${title}`));
|
|
795
|
+
console.log(chalk.blue(` ${prUrl}\n`));
|
|
796
|
+
} catch (err) {
|
|
797
|
+
createSpinner.fail("Erro ao criar PR: " + err.message);
|
|
798
|
+
process.exit(1);
|
|
799
|
+
}
|
|
800
|
+
});
|
|
801
|
+
|
|
802
|
+
// ==================== LIST ====================
|
|
803
|
+
program
|
|
804
|
+
.command("branches")
|
|
805
|
+
.description("Lista as branches do repositorio")
|
|
806
|
+
.action(async () => {
|
|
807
|
+
const config = await loadConfig();
|
|
808
|
+
if (!config) {
|
|
809
|
+
console.log(
|
|
810
|
+
chalk.red(
|
|
811
|
+
"\n Configuracao nao encontrada. Execute: azure-pr init\n"
|
|
812
|
+
)
|
|
813
|
+
);
|
|
814
|
+
process.exit(1);
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
const api = new AzureDevOpsApi(config);
|
|
818
|
+
const spinner = ora("Buscando branches...").start();
|
|
819
|
+
|
|
820
|
+
try {
|
|
821
|
+
const branches = await api.getBranches();
|
|
822
|
+
spinner.succeed(`${branches.length} branches:`);
|
|
823
|
+
branches.forEach((b) => {
|
|
824
|
+
console.log(` ${chalk.gray("-")} ${b.name}`);
|
|
825
|
+
});
|
|
826
|
+
console.log();
|
|
827
|
+
} catch (err) {
|
|
828
|
+
spinner.fail("Erro: " + err.message);
|
|
829
|
+
}
|
|
830
|
+
});
|
|
831
|
+
|
|
832
|
+
// ==================== TEST ====================
|
|
833
|
+
program
|
|
834
|
+
.command("test")
|
|
835
|
+
.description("Testa a conexao com o Azure DevOps")
|
|
836
|
+
.action(async () => {
|
|
837
|
+
const config = await loadConfig();
|
|
838
|
+
if (!config) {
|
|
839
|
+
console.log(
|
|
840
|
+
chalk.red(
|
|
841
|
+
"\n Configuracao nao encontrada. Execute: azure-pr init\n"
|
|
842
|
+
)
|
|
843
|
+
);
|
|
844
|
+
process.exit(1);
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
const api = new AzureDevOpsApi(config);
|
|
848
|
+
const spinner = ora("Testando conexao...").start();
|
|
849
|
+
|
|
850
|
+
const result = await api.testConnection();
|
|
851
|
+
if (result.success) {
|
|
852
|
+
spinner.succeed(
|
|
853
|
+
`Conexao OK! ${result.repos} repositorio(s) encontrado(s)`
|
|
854
|
+
);
|
|
855
|
+
} else {
|
|
856
|
+
spinner.fail("Falha: " + result.error);
|
|
857
|
+
}
|
|
858
|
+
});
|
|
859
|
+
|
|
860
|
+
program.parse();
|