@andre.buzeli/git-mcp 16.1.2 → 16.1.4
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 +14 -3
- package/src/index.js +58 -7
- package/src/resources/index.js +82 -44
- package/src/tools/git-clone.js +8 -0
- package/src/tools/git-diff.js +23 -4
- package/src/tools/git-help.js +8 -3
- package/src/tools/git-history.js +6 -2
- package/src/tools/git-merge.js +7 -5
- package/src/tools/git-remote.js +5 -1
- package/src/tools/git-stash.js +1 -0
- package/src/tools/git-workflow.js +98 -41
- package/src/tools/git-worktree.js +61 -36
- package/src/utils/gitAdapter.js +165 -125
- package/src/utils/retry.js +1 -1
- package/src/utils/worktreeResolver.js +189 -0
|
@@ -3,6 +3,7 @@ import fs from "fs";
|
|
|
3
3
|
import path from "path";
|
|
4
4
|
import { asToolError, asToolResult, errorToResponse, createError } from "../utils/errors.js";
|
|
5
5
|
import { getRepoNameFromPath, detectProjectType, GITIGNORE_TEMPLATES, validateProjectPath, withRetry } from "../utils/repoHelpers.js";
|
|
6
|
+
import { resolveWorktreeContext, isProtectedPath } from "../utils/worktreeResolver.js";
|
|
6
7
|
import { sendLog } from "../utils/mcpNotify.js";
|
|
7
8
|
|
|
8
9
|
const ajv = new Ajv({ allErrors: true });
|
|
@@ -40,7 +41,8 @@ export function createGitWorkflowTool(pm, git, server) {
|
|
|
40
41
|
files: {
|
|
41
42
|
type: "array",
|
|
42
43
|
items: { type: "string" },
|
|
43
|
-
|
|
44
|
+
default: ["."],
|
|
45
|
+
description: "Lista de arquivos para add/remove. Default: ['.'] (todos). Ex: ['src/index.js', 'package.json']"
|
|
44
46
|
},
|
|
45
47
|
message: {
|
|
46
48
|
type: "string",
|
|
@@ -61,22 +63,27 @@ export function createGitWorkflowTool(pm, git, server) {
|
|
|
61
63
|
},
|
|
62
64
|
force: {
|
|
63
65
|
type: "boolean",
|
|
66
|
+
default: false,
|
|
64
67
|
description: "Force push (use apenas se push normal falhar com erro de histórico divergente). Default: false"
|
|
65
68
|
},
|
|
66
69
|
createGitignore: {
|
|
67
70
|
type: "boolean",
|
|
71
|
+
default: true,
|
|
68
72
|
description: "Se true, cria .gitignore padrão baseado no tipo de projeto (action='init'). Default: true"
|
|
69
73
|
},
|
|
70
74
|
isPublic: {
|
|
71
75
|
type: "boolean",
|
|
76
|
+
default: false,
|
|
72
77
|
description: "Se true, repositório será PÚBLICO. Default: false (privado). Aplica-se a action='init' e 'ensure-remotes'"
|
|
73
78
|
},
|
|
74
79
|
dryRun: {
|
|
75
80
|
type: "boolean",
|
|
81
|
+
default: false,
|
|
76
82
|
description: "Se true, simula a operação sem executar (útil para testes). Default: false"
|
|
77
83
|
},
|
|
78
84
|
skipIfClean: {
|
|
79
85
|
type: "boolean",
|
|
86
|
+
default: false,
|
|
80
87
|
description: "Para action='update': se true, pula silenciosamente se não houver mudanças. Default: false"
|
|
81
88
|
},
|
|
82
89
|
gitignore: {
|
|
@@ -132,8 +139,32 @@ EXEMPLOS DE USO:
|
|
|
132
139
|
return asToolError("VALIDATION_ERROR", "Parâmetros inválidos", validate.errors);
|
|
133
140
|
}
|
|
134
141
|
const { projectPath, action } = args;
|
|
142
|
+
let effectivePath = projectPath;
|
|
143
|
+
let worktreeCtx = null;
|
|
144
|
+
|
|
135
145
|
try {
|
|
136
146
|
validateProjectPath(projectPath);
|
|
147
|
+
|
|
148
|
+
if (action === "init") {
|
|
149
|
+
if (isProtectedPath(projectPath)) {
|
|
150
|
+
return asToolError("PROTECTED_PATH", `Não é permitido criar repositório git em ${projectPath}`);
|
|
151
|
+
}
|
|
152
|
+
} else {
|
|
153
|
+
worktreeCtx = await resolveWorktreeContext(projectPath, git);
|
|
154
|
+
effectivePath = worktreeCtx.worktreePath;
|
|
155
|
+
|
|
156
|
+
// Ajustar paths relativos se estivermos rodando da raiz
|
|
157
|
+
if (effectivePath !== projectPath && Array.isArray(args.files)) {
|
|
158
|
+
const rel = path.relative(effectivePath, projectPath);
|
|
159
|
+
args.files = args.files.map(f => {
|
|
160
|
+
if (path.isAbsolute(f)) return f;
|
|
161
|
+
// Se for ".", vira o próprio diretório relativo
|
|
162
|
+
if (f === ".") return rel.replace(/\\/g, '/');
|
|
163
|
+
return path.join(rel, f).replace(/\\/g, '/');
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
137
168
|
if (action === "init") {
|
|
138
169
|
const shouldCreateGitignore = args.createGitignore !== false;
|
|
139
170
|
|
|
@@ -142,37 +173,37 @@ EXEMPLOS DE USO:
|
|
|
142
173
|
success: true,
|
|
143
174
|
dryRun: true,
|
|
144
175
|
message: "DRY RUN: Repositório seria inicializado localmente e nos providers",
|
|
145
|
-
repoName: getRepoNameFromPath(
|
|
176
|
+
repoName: getRepoNameFromPath(effectivePath),
|
|
146
177
|
gitignoreCreated: shouldCreateGitignore
|
|
147
178
|
});
|
|
148
179
|
}
|
|
149
180
|
|
|
150
|
-
const isRepo = await git.isRepo(
|
|
181
|
+
const isRepo = await git.isRepo(effectivePath);
|
|
151
182
|
if (!isRepo) {
|
|
152
|
-
await git.init(
|
|
183
|
+
await git.init(effectivePath);
|
|
153
184
|
}
|
|
154
185
|
|
|
155
186
|
// Criar .gitignore baseado no tipo de projeto (apenas se não existir ou se for novo repo)
|
|
156
187
|
let gitignoreCreated = false;
|
|
157
188
|
|
|
158
189
|
if (shouldCreateGitignore) {
|
|
159
|
-
const hasGitignore = fs.existsSync(path.join(
|
|
190
|
+
const hasGitignore = fs.existsSync(path.join(effectivePath, ".gitignore"));
|
|
160
191
|
if (!hasGitignore) {
|
|
161
|
-
const projectType = detectProjectType(
|
|
192
|
+
const projectType = detectProjectType(effectivePath);
|
|
162
193
|
const patterns = GITIGNORE_TEMPLATES[projectType] || GITIGNORE_TEMPLATES.general;
|
|
163
|
-
await git.createGitignore(
|
|
194
|
+
await git.createGitignore(effectivePath, patterns);
|
|
164
195
|
gitignoreCreated = true;
|
|
165
196
|
}
|
|
166
197
|
}
|
|
167
198
|
|
|
168
|
-
const repo = getRepoNameFromPath(
|
|
199
|
+
const repo = getRepoNameFromPath(effectivePath);
|
|
169
200
|
const isPublic = args.isPublic === true; // Default: privado
|
|
170
201
|
const organization = args.organization || undefined;
|
|
171
202
|
const ensured = await pm.ensureRepos({ repoName: repo, createIfMissing: true, isPublic, organization });
|
|
172
203
|
|
|
173
204
|
// Configurar remotes com org se fornecida
|
|
174
205
|
const urls = await pm.getRemoteUrls(repo, organization);
|
|
175
|
-
await git.ensureRemotes(
|
|
206
|
+
await git.ensureRemotes(effectivePath, { githubUrl: urls.github || "", giteaUrl: urls.gitea || "" });
|
|
176
207
|
|
|
177
208
|
return asToolResult({
|
|
178
209
|
success: true,
|
|
@@ -184,7 +215,7 @@ EXEMPLOS DE USO:
|
|
|
184
215
|
});
|
|
185
216
|
}
|
|
186
217
|
if (action === "status") {
|
|
187
|
-
const st = await git.status(
|
|
218
|
+
const st = await git.status(effectivePath);
|
|
188
219
|
|
|
189
220
|
if (args.dryRun) {
|
|
190
221
|
return asToolResult({
|
|
@@ -218,7 +249,15 @@ EXEMPLOS DE USO:
|
|
|
218
249
|
_aiContext.suggestedAction = "Working tree limpa. Modifique arquivos ou use action='push' se há commits pendentes";
|
|
219
250
|
}
|
|
220
251
|
|
|
221
|
-
|
|
252
|
+
const result = { ...st, _aiContext };
|
|
253
|
+
if (worktreeCtx?.isWorktree) {
|
|
254
|
+
result._worktreeContext = {
|
|
255
|
+
detected: true,
|
|
256
|
+
repoRoot: worktreeCtx.repoRoot,
|
|
257
|
+
branch: worktreeCtx.branch
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
return asToolResult(result, { tool: 'workflow', action: 'status' });
|
|
222
261
|
}
|
|
223
262
|
if (action === "add") {
|
|
224
263
|
const files = Array.isArray(args.files) && args.files.length ? args.files : ["."];
|
|
@@ -232,12 +271,12 @@ EXEMPLOS DE USO:
|
|
|
232
271
|
});
|
|
233
272
|
}
|
|
234
273
|
|
|
235
|
-
await git.add(
|
|
274
|
+
await git.add(effectivePath, files);
|
|
236
275
|
return asToolResult({ success: true, files }, { tool: 'workflow', action: 'add' });
|
|
237
276
|
}
|
|
238
277
|
if (action === "remove") {
|
|
239
278
|
const files = Array.isArray(args.files) ? args.files : [];
|
|
240
|
-
await git.remove(
|
|
279
|
+
await git.remove(effectivePath, files);
|
|
241
280
|
return asToolResult({ success: true, files });
|
|
242
281
|
}
|
|
243
282
|
if (action === "commit") {
|
|
@@ -253,12 +292,20 @@ EXEMPLOS DE USO:
|
|
|
253
292
|
});
|
|
254
293
|
}
|
|
255
294
|
|
|
256
|
-
const
|
|
295
|
+
const st = await git.status(effectivePath);
|
|
296
|
+
if (!st.staged || st.staged.length === 0) {
|
|
297
|
+
return asToolError("NOTHING_TO_COMMIT", "Nenhum arquivo staged para commitar", {
|
|
298
|
+
suggestion: "Use action='add' com files=['.'] para adicionar arquivos ao staging antes de commitar",
|
|
299
|
+
status: { modified: st.modified, notAdded: st.not_added }
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
const sha = await git.commit(effectivePath, args.message);
|
|
257
304
|
return asToolResult({ success: true, sha, message: args.message }, { tool: 'workflow', action: 'commit' });
|
|
258
305
|
}
|
|
259
306
|
if (action === "clean") {
|
|
260
307
|
if (args.dryRun) {
|
|
261
|
-
const result = await git.cleanUntracked(
|
|
308
|
+
const result = await git.cleanUntracked(effectivePath);
|
|
262
309
|
return asToolResult({
|
|
263
310
|
success: true,
|
|
264
311
|
dryRun: true,
|
|
@@ -267,7 +314,7 @@ EXEMPLOS DE USO:
|
|
|
267
314
|
});
|
|
268
315
|
}
|
|
269
316
|
|
|
270
|
-
const result = await git.cleanUntracked(
|
|
317
|
+
const result = await git.cleanUntracked(effectivePath);
|
|
271
318
|
return asToolResult({
|
|
272
319
|
success: true,
|
|
273
320
|
...result,
|
|
@@ -277,7 +324,7 @@ EXEMPLOS DE USO:
|
|
|
277
324
|
});
|
|
278
325
|
}
|
|
279
326
|
if (action === "ensure-remotes") {
|
|
280
|
-
const repo = getRepoNameFromPath(
|
|
327
|
+
const repo = getRepoNameFromPath(effectivePath);
|
|
281
328
|
const isPublic = args.isPublic === true; // Default: privado
|
|
282
329
|
const organization = args.organization || undefined;
|
|
283
330
|
|
|
@@ -304,12 +351,12 @@ EXEMPLOS DE USO:
|
|
|
304
351
|
const giteaBase = pm.giteaUrl?.replace(/\/$/, "");
|
|
305
352
|
const giteaUrl = ensured.gitea?.ok && ensured.gitea.repo && giteaBase
|
|
306
353
|
? `${giteaBase}/${ensured.gitea.repo}.git` : "";
|
|
307
|
-
await git.ensureRemotes(
|
|
308
|
-
const remotes = await git.listRemotes(
|
|
354
|
+
await git.ensureRemotes(effectivePath, { githubUrl, giteaUrl, organization });
|
|
355
|
+
const remotes = await git.listRemotes(effectivePath);
|
|
309
356
|
return asToolResult({ success: true, ensured, remotes, isPrivate: !isPublic, organization: organization || undefined }, { tool: 'workflow', action: 'ensure-remotes' });
|
|
310
357
|
}
|
|
311
358
|
if (action === "push") {
|
|
312
|
-
const branch = await git.getCurrentBranch(
|
|
359
|
+
const branch = await git.getCurrentBranch(effectivePath);
|
|
313
360
|
const force = !!args.force;
|
|
314
361
|
|
|
315
362
|
if (args.dryRun) {
|
|
@@ -329,7 +376,7 @@ EXEMPLOS DE USO:
|
|
|
329
376
|
|
|
330
377
|
// Retry logic for push (often fails due to network or concurrent updates)
|
|
331
378
|
const result = await withRetry(
|
|
332
|
-
() => git.pushParallel(
|
|
379
|
+
() => git.pushParallel(effectivePath, branch, force, organization),
|
|
333
380
|
3,
|
|
334
381
|
"push"
|
|
335
382
|
);
|
|
@@ -344,15 +391,15 @@ EXEMPLOS DE USO:
|
|
|
344
391
|
}
|
|
345
392
|
|
|
346
393
|
const channel = args.channel || "production";
|
|
347
|
-
const wtPath = path.join(projectPath, branch);
|
|
394
|
+
const wtPath = path.join(projectPath, branch); // Cria relativo ao path original solicitado
|
|
348
395
|
|
|
349
396
|
if (fs.existsSync(wtPath)) {
|
|
350
397
|
return asToolError("WORKTREE_PATH_EXISTS", `Diretório '${wtPath}' já existe.`, { suggestion: "Use git-worktree add se quiser configurar path customizado" });
|
|
351
398
|
}
|
|
352
399
|
|
|
353
400
|
try {
|
|
354
|
-
await git.addWorktree(
|
|
355
|
-
await git.setWorktreeConfig(
|
|
401
|
+
await git.addWorktree(effectivePath, branch, wtPath);
|
|
402
|
+
await git.setWorktreeConfig(effectivePath, branch, { path: wtPath, channel });
|
|
356
403
|
|
|
357
404
|
return asToolResult({
|
|
358
405
|
success: true,
|
|
@@ -384,13 +431,13 @@ EXEMPLOS DE USO:
|
|
|
384
431
|
if (args.dryRun) {
|
|
385
432
|
gitignored = gitignorePatterns;
|
|
386
433
|
} else {
|
|
387
|
-
await git.addToGitignore(
|
|
434
|
+
await git.addToGitignore(effectivePath, gitignorePatterns);
|
|
388
435
|
gitignored = gitignorePatterns;
|
|
389
436
|
}
|
|
390
437
|
}
|
|
391
438
|
|
|
392
439
|
// 1. Status para ver o que mudou
|
|
393
|
-
const status = await git.status(
|
|
440
|
+
const status = await git.status(effectivePath);
|
|
394
441
|
|
|
395
442
|
const hasChanges = !status.isClean ||
|
|
396
443
|
(status.not_added?.length > 0) ||
|
|
@@ -432,15 +479,15 @@ EXEMPLOS DE USO:
|
|
|
432
479
|
}
|
|
433
480
|
|
|
434
481
|
// 2. Add
|
|
435
|
-
await git.add(
|
|
482
|
+
await git.add(effectivePath, files);
|
|
436
483
|
|
|
437
484
|
// 3. Commit
|
|
438
|
-
const sha = await git.commit(
|
|
485
|
+
const sha = await git.commit(effectivePath, args.message);
|
|
439
486
|
|
|
440
487
|
// 3.5. Garantir remotes com organization (se fornecida)
|
|
441
488
|
const organization = args.organization || undefined;
|
|
442
489
|
if (organization) {
|
|
443
|
-
const repo = getRepoNameFromPath(
|
|
490
|
+
const repo = getRepoNameFromPath(effectivePath);
|
|
444
491
|
const isPublic = args.isPublic === true;
|
|
445
492
|
const ensured = await pm.ensureRepos({ repoName: repo, createIfMissing: true, isPublic, organization });
|
|
446
493
|
// Build URLs from actual ensureRepos results (handles GitHub fallback to personal account)
|
|
@@ -449,11 +496,11 @@ EXEMPLOS DE USO:
|
|
|
449
496
|
const giteaBase = pm.giteaUrl?.replace(/\/$/, "");
|
|
450
497
|
const giteaUrl = ensured.gitea?.ok && ensured.gitea.repo && giteaBase
|
|
451
498
|
? `${giteaBase}/${ensured.gitea.repo}.git` : "";
|
|
452
|
-
await git.ensureRemotes(
|
|
499
|
+
await git.ensureRemotes(effectivePath, { githubUrl, giteaUrl, organization });
|
|
453
500
|
}
|
|
454
501
|
|
|
455
502
|
// 4. Push Strategy
|
|
456
|
-
const branch = await git.getCurrentBranch(
|
|
503
|
+
const branch = await git.getCurrentBranch(effectivePath);
|
|
457
504
|
let pushResult = {};
|
|
458
505
|
let synced = [];
|
|
459
506
|
let errors = [];
|
|
@@ -461,7 +508,7 @@ EXEMPLOS DE USO:
|
|
|
461
508
|
// Determine Channel
|
|
462
509
|
let channel = args.channel;
|
|
463
510
|
if (!channel) {
|
|
464
|
-
const storedChannel = await git.getConfig(
|
|
511
|
+
const storedChannel = await git.getConfig(effectivePath, `worktree-branch.${branch}.channel`);
|
|
465
512
|
if (storedChannel) channel = storedChannel;
|
|
466
513
|
}
|
|
467
514
|
|
|
@@ -471,13 +518,13 @@ EXEMPLOS DE USO:
|
|
|
471
518
|
// SYNC BRANCHES
|
|
472
519
|
if (syncBranches) {
|
|
473
520
|
// Só permitido no principal (onde .git é diretório)
|
|
474
|
-
if (fs.existsSync(path.join(
|
|
521
|
+
if (fs.existsSync(path.join(effectivePath, ".git")) && fs.statSync(path.join(effectivePath, ".git")).isFile()) {
|
|
475
522
|
return asToolError("INVALID_OPERATION", "syncBranches=true só pode ser executado a partir do repositório principal, não de um worktree.");
|
|
476
523
|
}
|
|
477
524
|
|
|
478
525
|
// 1. Push do principal (current)
|
|
479
526
|
const remoteBranch = resolveRemoteBranch(branch, channel);
|
|
480
|
-
const remotes = await git.listRemotes(
|
|
527
|
+
const remotes = await git.listRemotes(effectivePath);
|
|
481
528
|
|
|
482
529
|
await sendLog(server, "info", "update: push iniciado (sync)", { branch, remoteBranch, force });
|
|
483
530
|
|
|
@@ -485,7 +532,7 @@ EXEMPLOS DE USO:
|
|
|
485
532
|
const mainFailed = [];
|
|
486
533
|
for (const r of remotes) {
|
|
487
534
|
try {
|
|
488
|
-
await git.pushRefspec(
|
|
535
|
+
await git.pushRefspec(effectivePath, r.remote, branch, remoteBranch, force);
|
|
489
536
|
mainPushed.push(r.remote);
|
|
490
537
|
} catch(e) {
|
|
491
538
|
mainFailed.push({ remote: r.remote, error: e.message });
|
|
@@ -495,7 +542,7 @@ EXEMPLOS DE USO:
|
|
|
495
542
|
// 2. Propagar para worktrees
|
|
496
543
|
// Req 8.3: synced.length + errors.length === N (número de worktrees registrados)
|
|
497
544
|
// O principal NÃO entra no contador — só os worktrees registrados
|
|
498
|
-
const configs = await git.getWorktreeConfigs(
|
|
545
|
+
const configs = await git.getWorktreeConfigs(effectivePath);
|
|
499
546
|
|
|
500
547
|
for (const wt of configs) {
|
|
501
548
|
// Pula se for a própria branch principal (caso esteja registrada)
|
|
@@ -532,13 +579,13 @@ EXEMPLOS DE USO:
|
|
|
532
579
|
// Comportamento padrão (production)
|
|
533
580
|
await sendLog(server, "info", "update: push iniciado", { branch, force });
|
|
534
581
|
pushResult = await withRetry(
|
|
535
|
-
() => git.pushParallel(
|
|
582
|
+
() => git.pushParallel(effectivePath, branch, force, organization),
|
|
536
583
|
3,
|
|
537
584
|
"push"
|
|
538
585
|
);
|
|
539
586
|
} else {
|
|
540
587
|
// Comportamento customizado (beta/alpha) -> pushRefspec
|
|
541
|
-
const remotes = await git.listRemotes(
|
|
588
|
+
const remotes = await git.listRemotes(effectivePath);
|
|
542
589
|
|
|
543
590
|
const pushed = [];
|
|
544
591
|
const failed = [];
|
|
@@ -547,7 +594,7 @@ EXEMPLOS DE USO:
|
|
|
547
594
|
|
|
548
595
|
for (const r of remotes) {
|
|
549
596
|
try {
|
|
550
|
-
await git.pushRefspec(
|
|
597
|
+
await git.pushRefspec(effectivePath, r.remote, branch, remoteBranch, force);
|
|
551
598
|
pushed.push(r.remote);
|
|
552
599
|
} catch (e) {
|
|
553
600
|
failed.push({ remote: r.remote, error: e.message });
|
|
@@ -562,7 +609,7 @@ EXEMPLOS DE USO:
|
|
|
562
609
|
}
|
|
563
610
|
}
|
|
564
611
|
|
|
565
|
-
|
|
612
|
+
const result = {
|
|
566
613
|
success: true,
|
|
567
614
|
action: "update",
|
|
568
615
|
steps: ["status", "add", "commit", "push"],
|
|
@@ -577,7 +624,17 @@ EXEMPLOS DE USO:
|
|
|
577
624
|
completed: true,
|
|
578
625
|
message: "Ciclo completo: arquivos adicionados, commit criado e push realizado" + (organization ? ` [org: ${organization}]` : "")
|
|
579
626
|
}
|
|
580
|
-
}
|
|
627
|
+
};
|
|
628
|
+
|
|
629
|
+
if (worktreeCtx?.isWorktree) {
|
|
630
|
+
result._worktreeContext = {
|
|
631
|
+
detected: true,
|
|
632
|
+
repoRoot: worktreeCtx.repoRoot,
|
|
633
|
+
branch: worktreeCtx.branch
|
|
634
|
+
};
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
return asToolResult(result, { tool: 'workflow', action: 'update' });
|
|
581
638
|
}
|
|
582
639
|
|
|
583
640
|
return asToolError("VALIDATION_ERROR", `Ação '${action}' não suportada`, {
|
|
@@ -3,6 +3,7 @@ import fs from "fs";
|
|
|
3
3
|
import path from "path";
|
|
4
4
|
import { asToolError, asToolResult, errorToResponse } from "../utils/errors.js";
|
|
5
5
|
import { validateProjectPath, withRetry } from "../utils/repoHelpers.js";
|
|
6
|
+
import { resolveWorktreeContext, isProtectedPath } from "../utils/worktreeResolver.js";
|
|
6
7
|
|
|
7
8
|
const ajv = new Ajv({ allErrors: true });
|
|
8
9
|
|
|
@@ -75,62 +76,59 @@ AÇÕES:
|
|
|
75
76
|
const validate = ajv.compile(inputSchema);
|
|
76
77
|
if (!validate(args || {})) return asToolError("VALIDATION_ERROR", "Parâmetros inválidos", validate.errors);
|
|
77
78
|
const { projectPath, action } = args;
|
|
79
|
+
let effectivePath = projectPath;
|
|
80
|
+
let worktreeCtx = null;
|
|
78
81
|
|
|
79
82
|
try {
|
|
80
83
|
validateProjectPath(projectPath);
|
|
81
84
|
|
|
85
|
+
worktreeCtx = await resolveWorktreeContext(projectPath, git);
|
|
86
|
+
effectivePath = worktreeCtx.worktreePath;
|
|
87
|
+
|
|
88
|
+
const _worktreeContext = worktreeCtx.isWorktree ? {
|
|
89
|
+
detected: true,
|
|
90
|
+
repoRoot: worktreeCtx.repoRoot,
|
|
91
|
+
branch: worktreeCtx.branch
|
|
92
|
+
} : undefined;
|
|
93
|
+
|
|
82
94
|
if (action === "list") {
|
|
83
|
-
const worktrees = await git.listWorktrees(
|
|
95
|
+
const worktrees = await git.listWorktrees(effectivePath);
|
|
84
96
|
|
|
85
97
|
// Enrich with channel info if available
|
|
86
98
|
const enriched = [];
|
|
87
99
|
for (const wt of worktrees) {
|
|
88
|
-
const channel = await git.getConfig(
|
|
100
|
+
const channel = await git.getConfig(effectivePath, `worktree-branch.${wt.branch}.channel`);
|
|
89
101
|
enriched.push({ ...wt, channel: channel || "production" }); // Default to production
|
|
90
102
|
}
|
|
91
103
|
|
|
92
|
-
return asToolResult({ worktrees: enriched, count: enriched.length });
|
|
104
|
+
return asToolResult({ worktrees: enriched, count: enriched.length, _worktreeContext });
|
|
93
105
|
}
|
|
94
106
|
|
|
95
107
|
if (action === "add") {
|
|
96
108
|
if (!args.branch) return asToolError("MISSING_PARAMETER", "branch é obrigatório para add", { parameter: "branch" });
|
|
97
109
|
|
|
98
|
-
//
|
|
99
|
-
|
|
100
|
-
// Mas git worktree add path branch.
|
|
101
|
-
|
|
102
|
-
// Se path não fornecido, assumir irmão do diretório atual se estivermos em um repo.
|
|
103
|
-
// Mas projectPath é a raiz do repo.
|
|
104
|
-
// Vamos usar path relativo ou absoluto fornecido, ou default: join(projectPath, "..", branch)
|
|
105
|
-
|
|
106
|
-
const targetPath = args.path
|
|
110
|
+
// Cria relativo ao projectPath original se path não for absoluto
|
|
111
|
+
const wtPath = args.path
|
|
107
112
|
? path.resolve(projectPath, args.path)
|
|
108
|
-
: path.join(projectPath, args.branch);
|
|
109
|
-
|
|
110
|
-
// Melhor prática: criar dentro se for estrutura monorepo-style, ou fora.
|
|
111
|
-
// O git-mcp v16 parece usar estrutura plana ou aninhada.
|
|
112
|
-
// Vamos usar o comportamento padrão do git: path relativo à raiz.
|
|
113
|
-
// Se o usuário passar apenas o nome da branch como path, o git cria ./branch-name.
|
|
114
|
-
|
|
115
|
-
// Se path não informado, usa o nome da branch (cria pasta ./branch-name dentro do repo atual)
|
|
116
|
-
// Isso pode sujar o repo principal se não estiver no gitignore.
|
|
117
|
-
// Vamos sugerir ou usar ../branch-name se estivermos na raiz?
|
|
118
|
-
// Simplicidade: user define path ou usa ./branch-name.
|
|
119
|
-
|
|
120
|
-
const wtPath = args.path || args.branch;
|
|
113
|
+
: path.join(projectPath, args.branch);
|
|
121
114
|
|
|
122
|
-
|
|
115
|
+
if (isProtectedPath(wtPath)) {
|
|
116
|
+
return asToolError("PROTECTED_PATH", `Não é permitido criar worktree em ${wtPath}`);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
await withRetry(() => git.addWorktree(effectivePath, args.branch, wtPath, args.force), 3, "worktree-add");
|
|
123
120
|
|
|
124
121
|
// Set channel if provided
|
|
125
122
|
if (args.channel) {
|
|
126
|
-
await git.setWorktreeConfig(
|
|
123
|
+
await git.setWorktreeConfig(effectivePath, args.branch, { channel: args.channel });
|
|
127
124
|
}
|
|
128
125
|
|
|
129
126
|
return asToolResult({
|
|
130
127
|
success: true,
|
|
131
128
|
branch: args.branch,
|
|
132
|
-
path:
|
|
133
|
-
channel: args.channel || "production"
|
|
129
|
+
path: wtPath,
|
|
130
|
+
channel: args.channel || "production",
|
|
131
|
+
_worktreeContext
|
|
134
132
|
});
|
|
135
133
|
}
|
|
136
134
|
|
|
@@ -141,27 +139,54 @@ AÇÕES:
|
|
|
141
139
|
// Precisamos encontrar o path se só branch for fornecida
|
|
142
140
|
let targetPath = args.path;
|
|
143
141
|
if (!targetPath && args.branch) {
|
|
144
|
-
const wts = await git.listWorktrees(
|
|
142
|
+
const wts = await git.listWorktrees(effectivePath);
|
|
145
143
|
const found = wts.find(w => w.branch === args.branch);
|
|
146
144
|
if (!found) return asToolError("WORKTREE_NOT_FOUND", `Worktree para branch '${args.branch}' não encontrado`);
|
|
147
145
|
targetPath = found.path;
|
|
148
146
|
}
|
|
149
147
|
|
|
150
|
-
await withRetry(() => git.removeWorktree(
|
|
151
|
-
return asToolResult({ success: true, removed: targetPath });
|
|
148
|
+
await withRetry(() => git.removeWorktree(effectivePath, targetPath, args.force), 3, "worktree-remove");
|
|
149
|
+
return asToolResult({ success: true, removed: targetPath, _worktreeContext });
|
|
152
150
|
}
|
|
153
151
|
|
|
154
152
|
if (action === "prune") {
|
|
155
|
-
await git.pruneWorktrees(
|
|
156
|
-
return asToolResult({ success: true, message: "Worktrees podados" });
|
|
153
|
+
await git.pruneWorktrees(effectivePath);
|
|
154
|
+
return asToolResult({ success: true, message: "Worktrees podados", _worktreeContext });
|
|
157
155
|
}
|
|
158
156
|
|
|
159
157
|
if (action === "set-channel") {
|
|
160
158
|
if (!args.branch) return asToolError("MISSING_PARAMETER", "branch é obrigatório");
|
|
161
159
|
if (!args.channel) return asToolError("MISSING_PARAMETER", "channel é obrigatório");
|
|
162
160
|
|
|
163
|
-
await git.setWorktreeConfig(
|
|
164
|
-
return asToolResult({ success: true, branch: args.branch, channel: args.channel });
|
|
161
|
+
await git.setWorktreeConfig(effectivePath, args.branch, { channel: args.channel });
|
|
162
|
+
return asToolResult({ success: true, branch: args.branch, channel: args.channel, _worktreeContext });
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (action === "setup") {
|
|
166
|
+
// Setup action logic - Estrutura recomendada
|
|
167
|
+
// 1. Verifica se está num repo
|
|
168
|
+
// 2. Se não estiver, init
|
|
169
|
+
// 3. Move master para subpasta 'main' se estiver na raiz?
|
|
170
|
+
// Simplificação: Apenas cria as pastas se não existirem
|
|
171
|
+
|
|
172
|
+
const mainPath = path.join(effectivePath, 'main');
|
|
173
|
+
const worktreesPath = path.join(effectivePath, 'worktrees');
|
|
174
|
+
|
|
175
|
+
if (!fs.existsSync(mainPath)) {
|
|
176
|
+
// Se estamos na raiz de um repo normal, setup é meio redundante se não formos mover arquivos.
|
|
177
|
+
// Mas se o usuário quer estruturar do zero:
|
|
178
|
+
// git init bare? Não, o spec não diz bare.
|
|
179
|
+
|
|
180
|
+
// Vamos apenas retornar uma mensagem informativa por enquanto, pois a migração automática é arriscada.
|
|
181
|
+
return asToolResult({
|
|
182
|
+
success: true,
|
|
183
|
+
message: "Para configurar worktrees, recomenda-se mover o branch principal para uma pasta 'main' e criar novos worktrees em pastas irmãs.",
|
|
184
|
+
structure: {
|
|
185
|
+
root: projectPath,
|
|
186
|
+
recommended: ["main/", "feature-branch/", "hotfix/"]
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
}
|
|
165
190
|
}
|
|
166
191
|
|
|
167
192
|
return asToolError("VALIDATION_ERROR", `Ação '${action}' não suportada`, { availableActions: ["add", "list", "remove", "prune", "set-channel"] });
|