@andre.buzeli/git-mcp 16.1.3 → 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 +8 -3
- package/src/resources/index.js +79 -41
- package/src/tools/git-clone.js +8 -0
- package/src/tools/git-diff.js +22 -4
- package/src/tools/git-help.js +8 -3
- package/src/tools/git-history.js +4 -2
- package/src/tools/git-merge.js +7 -5
- package/src/tools/git-remote.js +5 -1
- package/src/tools/git-workflow.js +91 -40
- 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 });
|
|
@@ -138,8 +139,32 @@ EXEMPLOS DE USO:
|
|
|
138
139
|
return asToolError("VALIDATION_ERROR", "Parâmetros inválidos", validate.errors);
|
|
139
140
|
}
|
|
140
141
|
const { projectPath, action } = args;
|
|
142
|
+
let effectivePath = projectPath;
|
|
143
|
+
let worktreeCtx = null;
|
|
144
|
+
|
|
141
145
|
try {
|
|
142
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
|
+
|
|
143
168
|
if (action === "init") {
|
|
144
169
|
const shouldCreateGitignore = args.createGitignore !== false;
|
|
145
170
|
|
|
@@ -148,37 +173,37 @@ EXEMPLOS DE USO:
|
|
|
148
173
|
success: true,
|
|
149
174
|
dryRun: true,
|
|
150
175
|
message: "DRY RUN: Repositório seria inicializado localmente e nos providers",
|
|
151
|
-
repoName: getRepoNameFromPath(
|
|
176
|
+
repoName: getRepoNameFromPath(effectivePath),
|
|
152
177
|
gitignoreCreated: shouldCreateGitignore
|
|
153
178
|
});
|
|
154
179
|
}
|
|
155
180
|
|
|
156
|
-
const isRepo = await git.isRepo(
|
|
181
|
+
const isRepo = await git.isRepo(effectivePath);
|
|
157
182
|
if (!isRepo) {
|
|
158
|
-
await git.init(
|
|
183
|
+
await git.init(effectivePath);
|
|
159
184
|
}
|
|
160
185
|
|
|
161
186
|
// Criar .gitignore baseado no tipo de projeto (apenas se não existir ou se for novo repo)
|
|
162
187
|
let gitignoreCreated = false;
|
|
163
188
|
|
|
164
189
|
if (shouldCreateGitignore) {
|
|
165
|
-
const hasGitignore = fs.existsSync(path.join(
|
|
190
|
+
const hasGitignore = fs.existsSync(path.join(effectivePath, ".gitignore"));
|
|
166
191
|
if (!hasGitignore) {
|
|
167
|
-
const projectType = detectProjectType(
|
|
192
|
+
const projectType = detectProjectType(effectivePath);
|
|
168
193
|
const patterns = GITIGNORE_TEMPLATES[projectType] || GITIGNORE_TEMPLATES.general;
|
|
169
|
-
await git.createGitignore(
|
|
194
|
+
await git.createGitignore(effectivePath, patterns);
|
|
170
195
|
gitignoreCreated = true;
|
|
171
196
|
}
|
|
172
197
|
}
|
|
173
198
|
|
|
174
|
-
const repo = getRepoNameFromPath(
|
|
199
|
+
const repo = getRepoNameFromPath(effectivePath);
|
|
175
200
|
const isPublic = args.isPublic === true; // Default: privado
|
|
176
201
|
const organization = args.organization || undefined;
|
|
177
202
|
const ensured = await pm.ensureRepos({ repoName: repo, createIfMissing: true, isPublic, organization });
|
|
178
203
|
|
|
179
204
|
// Configurar remotes com org se fornecida
|
|
180
205
|
const urls = await pm.getRemoteUrls(repo, organization);
|
|
181
|
-
await git.ensureRemotes(
|
|
206
|
+
await git.ensureRemotes(effectivePath, { githubUrl: urls.github || "", giteaUrl: urls.gitea || "" });
|
|
182
207
|
|
|
183
208
|
return asToolResult({
|
|
184
209
|
success: true,
|
|
@@ -190,7 +215,7 @@ EXEMPLOS DE USO:
|
|
|
190
215
|
});
|
|
191
216
|
}
|
|
192
217
|
if (action === "status") {
|
|
193
|
-
const st = await git.status(
|
|
218
|
+
const st = await git.status(effectivePath);
|
|
194
219
|
|
|
195
220
|
if (args.dryRun) {
|
|
196
221
|
return asToolResult({
|
|
@@ -224,7 +249,15 @@ EXEMPLOS DE USO:
|
|
|
224
249
|
_aiContext.suggestedAction = "Working tree limpa. Modifique arquivos ou use action='push' se há commits pendentes";
|
|
225
250
|
}
|
|
226
251
|
|
|
227
|
-
|
|
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' });
|
|
228
261
|
}
|
|
229
262
|
if (action === "add") {
|
|
230
263
|
const files = Array.isArray(args.files) && args.files.length ? args.files : ["."];
|
|
@@ -238,12 +271,12 @@ EXEMPLOS DE USO:
|
|
|
238
271
|
});
|
|
239
272
|
}
|
|
240
273
|
|
|
241
|
-
await git.add(
|
|
274
|
+
await git.add(effectivePath, files);
|
|
242
275
|
return asToolResult({ success: true, files }, { tool: 'workflow', action: 'add' });
|
|
243
276
|
}
|
|
244
277
|
if (action === "remove") {
|
|
245
278
|
const files = Array.isArray(args.files) ? args.files : [];
|
|
246
|
-
await git.remove(
|
|
279
|
+
await git.remove(effectivePath, files);
|
|
247
280
|
return asToolResult({ success: true, files });
|
|
248
281
|
}
|
|
249
282
|
if (action === "commit") {
|
|
@@ -259,12 +292,20 @@ EXEMPLOS DE USO:
|
|
|
259
292
|
});
|
|
260
293
|
}
|
|
261
294
|
|
|
262
|
-
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);
|
|
263
304
|
return asToolResult({ success: true, sha, message: args.message }, { tool: 'workflow', action: 'commit' });
|
|
264
305
|
}
|
|
265
306
|
if (action === "clean") {
|
|
266
307
|
if (args.dryRun) {
|
|
267
|
-
const result = await git.cleanUntracked(
|
|
308
|
+
const result = await git.cleanUntracked(effectivePath);
|
|
268
309
|
return asToolResult({
|
|
269
310
|
success: true,
|
|
270
311
|
dryRun: true,
|
|
@@ -273,7 +314,7 @@ EXEMPLOS DE USO:
|
|
|
273
314
|
});
|
|
274
315
|
}
|
|
275
316
|
|
|
276
|
-
const result = await git.cleanUntracked(
|
|
317
|
+
const result = await git.cleanUntracked(effectivePath);
|
|
277
318
|
return asToolResult({
|
|
278
319
|
success: true,
|
|
279
320
|
...result,
|
|
@@ -283,7 +324,7 @@ EXEMPLOS DE USO:
|
|
|
283
324
|
});
|
|
284
325
|
}
|
|
285
326
|
if (action === "ensure-remotes") {
|
|
286
|
-
const repo = getRepoNameFromPath(
|
|
327
|
+
const repo = getRepoNameFromPath(effectivePath);
|
|
287
328
|
const isPublic = args.isPublic === true; // Default: privado
|
|
288
329
|
const organization = args.organization || undefined;
|
|
289
330
|
|
|
@@ -310,12 +351,12 @@ EXEMPLOS DE USO:
|
|
|
310
351
|
const giteaBase = pm.giteaUrl?.replace(/\/$/, "");
|
|
311
352
|
const giteaUrl = ensured.gitea?.ok && ensured.gitea.repo && giteaBase
|
|
312
353
|
? `${giteaBase}/${ensured.gitea.repo}.git` : "";
|
|
313
|
-
await git.ensureRemotes(
|
|
314
|
-
const remotes = await git.listRemotes(
|
|
354
|
+
await git.ensureRemotes(effectivePath, { githubUrl, giteaUrl, organization });
|
|
355
|
+
const remotes = await git.listRemotes(effectivePath);
|
|
315
356
|
return asToolResult({ success: true, ensured, remotes, isPrivate: !isPublic, organization: organization || undefined }, { tool: 'workflow', action: 'ensure-remotes' });
|
|
316
357
|
}
|
|
317
358
|
if (action === "push") {
|
|
318
|
-
const branch = await git.getCurrentBranch(
|
|
359
|
+
const branch = await git.getCurrentBranch(effectivePath);
|
|
319
360
|
const force = !!args.force;
|
|
320
361
|
|
|
321
362
|
if (args.dryRun) {
|
|
@@ -335,7 +376,7 @@ EXEMPLOS DE USO:
|
|
|
335
376
|
|
|
336
377
|
// Retry logic for push (often fails due to network or concurrent updates)
|
|
337
378
|
const result = await withRetry(
|
|
338
|
-
() => git.pushParallel(
|
|
379
|
+
() => git.pushParallel(effectivePath, branch, force, organization),
|
|
339
380
|
3,
|
|
340
381
|
"push"
|
|
341
382
|
);
|
|
@@ -350,15 +391,15 @@ EXEMPLOS DE USO:
|
|
|
350
391
|
}
|
|
351
392
|
|
|
352
393
|
const channel = args.channel || "production";
|
|
353
|
-
const wtPath = path.join(projectPath, branch);
|
|
394
|
+
const wtPath = path.join(projectPath, branch); // Cria relativo ao path original solicitado
|
|
354
395
|
|
|
355
396
|
if (fs.existsSync(wtPath)) {
|
|
356
397
|
return asToolError("WORKTREE_PATH_EXISTS", `Diretório '${wtPath}' já existe.`, { suggestion: "Use git-worktree add se quiser configurar path customizado" });
|
|
357
398
|
}
|
|
358
399
|
|
|
359
400
|
try {
|
|
360
|
-
await git.addWorktree(
|
|
361
|
-
await git.setWorktreeConfig(
|
|
401
|
+
await git.addWorktree(effectivePath, branch, wtPath);
|
|
402
|
+
await git.setWorktreeConfig(effectivePath, branch, { path: wtPath, channel });
|
|
362
403
|
|
|
363
404
|
return asToolResult({
|
|
364
405
|
success: true,
|
|
@@ -390,13 +431,13 @@ EXEMPLOS DE USO:
|
|
|
390
431
|
if (args.dryRun) {
|
|
391
432
|
gitignored = gitignorePatterns;
|
|
392
433
|
} else {
|
|
393
|
-
await git.addToGitignore(
|
|
434
|
+
await git.addToGitignore(effectivePath, gitignorePatterns);
|
|
394
435
|
gitignored = gitignorePatterns;
|
|
395
436
|
}
|
|
396
437
|
}
|
|
397
438
|
|
|
398
439
|
// 1. Status para ver o que mudou
|
|
399
|
-
const status = await git.status(
|
|
440
|
+
const status = await git.status(effectivePath);
|
|
400
441
|
|
|
401
442
|
const hasChanges = !status.isClean ||
|
|
402
443
|
(status.not_added?.length > 0) ||
|
|
@@ -438,15 +479,15 @@ EXEMPLOS DE USO:
|
|
|
438
479
|
}
|
|
439
480
|
|
|
440
481
|
// 2. Add
|
|
441
|
-
await git.add(
|
|
482
|
+
await git.add(effectivePath, files);
|
|
442
483
|
|
|
443
484
|
// 3. Commit
|
|
444
|
-
const sha = await git.commit(
|
|
485
|
+
const sha = await git.commit(effectivePath, args.message);
|
|
445
486
|
|
|
446
487
|
// 3.5. Garantir remotes com organization (se fornecida)
|
|
447
488
|
const organization = args.organization || undefined;
|
|
448
489
|
if (organization) {
|
|
449
|
-
const repo = getRepoNameFromPath(
|
|
490
|
+
const repo = getRepoNameFromPath(effectivePath);
|
|
450
491
|
const isPublic = args.isPublic === true;
|
|
451
492
|
const ensured = await pm.ensureRepos({ repoName: repo, createIfMissing: true, isPublic, organization });
|
|
452
493
|
// Build URLs from actual ensureRepos results (handles GitHub fallback to personal account)
|
|
@@ -455,11 +496,11 @@ EXEMPLOS DE USO:
|
|
|
455
496
|
const giteaBase = pm.giteaUrl?.replace(/\/$/, "");
|
|
456
497
|
const giteaUrl = ensured.gitea?.ok && ensured.gitea.repo && giteaBase
|
|
457
498
|
? `${giteaBase}/${ensured.gitea.repo}.git` : "";
|
|
458
|
-
await git.ensureRemotes(
|
|
499
|
+
await git.ensureRemotes(effectivePath, { githubUrl, giteaUrl, organization });
|
|
459
500
|
}
|
|
460
501
|
|
|
461
502
|
// 4. Push Strategy
|
|
462
|
-
const branch = await git.getCurrentBranch(
|
|
503
|
+
const branch = await git.getCurrentBranch(effectivePath);
|
|
463
504
|
let pushResult = {};
|
|
464
505
|
let synced = [];
|
|
465
506
|
let errors = [];
|
|
@@ -467,7 +508,7 @@ EXEMPLOS DE USO:
|
|
|
467
508
|
// Determine Channel
|
|
468
509
|
let channel = args.channel;
|
|
469
510
|
if (!channel) {
|
|
470
|
-
const storedChannel = await git.getConfig(
|
|
511
|
+
const storedChannel = await git.getConfig(effectivePath, `worktree-branch.${branch}.channel`);
|
|
471
512
|
if (storedChannel) channel = storedChannel;
|
|
472
513
|
}
|
|
473
514
|
|
|
@@ -477,13 +518,13 @@ EXEMPLOS DE USO:
|
|
|
477
518
|
// SYNC BRANCHES
|
|
478
519
|
if (syncBranches) {
|
|
479
520
|
// Só permitido no principal (onde .git é diretório)
|
|
480
|
-
if (fs.existsSync(path.join(
|
|
521
|
+
if (fs.existsSync(path.join(effectivePath, ".git")) && fs.statSync(path.join(effectivePath, ".git")).isFile()) {
|
|
481
522
|
return asToolError("INVALID_OPERATION", "syncBranches=true só pode ser executado a partir do repositório principal, não de um worktree.");
|
|
482
523
|
}
|
|
483
524
|
|
|
484
525
|
// 1. Push do principal (current)
|
|
485
526
|
const remoteBranch = resolveRemoteBranch(branch, channel);
|
|
486
|
-
const remotes = await git.listRemotes(
|
|
527
|
+
const remotes = await git.listRemotes(effectivePath);
|
|
487
528
|
|
|
488
529
|
await sendLog(server, "info", "update: push iniciado (sync)", { branch, remoteBranch, force });
|
|
489
530
|
|
|
@@ -491,7 +532,7 @@ EXEMPLOS DE USO:
|
|
|
491
532
|
const mainFailed = [];
|
|
492
533
|
for (const r of remotes) {
|
|
493
534
|
try {
|
|
494
|
-
await git.pushRefspec(
|
|
535
|
+
await git.pushRefspec(effectivePath, r.remote, branch, remoteBranch, force);
|
|
495
536
|
mainPushed.push(r.remote);
|
|
496
537
|
} catch(e) {
|
|
497
538
|
mainFailed.push({ remote: r.remote, error: e.message });
|
|
@@ -501,7 +542,7 @@ EXEMPLOS DE USO:
|
|
|
501
542
|
// 2. Propagar para worktrees
|
|
502
543
|
// Req 8.3: synced.length + errors.length === N (número de worktrees registrados)
|
|
503
544
|
// O principal NÃO entra no contador — só os worktrees registrados
|
|
504
|
-
const configs = await git.getWorktreeConfigs(
|
|
545
|
+
const configs = await git.getWorktreeConfigs(effectivePath);
|
|
505
546
|
|
|
506
547
|
for (const wt of configs) {
|
|
507
548
|
// Pula se for a própria branch principal (caso esteja registrada)
|
|
@@ -538,13 +579,13 @@ EXEMPLOS DE USO:
|
|
|
538
579
|
// Comportamento padrão (production)
|
|
539
580
|
await sendLog(server, "info", "update: push iniciado", { branch, force });
|
|
540
581
|
pushResult = await withRetry(
|
|
541
|
-
() => git.pushParallel(
|
|
582
|
+
() => git.pushParallel(effectivePath, branch, force, organization),
|
|
542
583
|
3,
|
|
543
584
|
"push"
|
|
544
585
|
);
|
|
545
586
|
} else {
|
|
546
587
|
// Comportamento customizado (beta/alpha) -> pushRefspec
|
|
547
|
-
const remotes = await git.listRemotes(
|
|
588
|
+
const remotes = await git.listRemotes(effectivePath);
|
|
548
589
|
|
|
549
590
|
const pushed = [];
|
|
550
591
|
const failed = [];
|
|
@@ -553,7 +594,7 @@ EXEMPLOS DE USO:
|
|
|
553
594
|
|
|
554
595
|
for (const r of remotes) {
|
|
555
596
|
try {
|
|
556
|
-
await git.pushRefspec(
|
|
597
|
+
await git.pushRefspec(effectivePath, r.remote, branch, remoteBranch, force);
|
|
557
598
|
pushed.push(r.remote);
|
|
558
599
|
} catch (e) {
|
|
559
600
|
failed.push({ remote: r.remote, error: e.message });
|
|
@@ -568,7 +609,7 @@ EXEMPLOS DE USO:
|
|
|
568
609
|
}
|
|
569
610
|
}
|
|
570
611
|
|
|
571
|
-
|
|
612
|
+
const result = {
|
|
572
613
|
success: true,
|
|
573
614
|
action: "update",
|
|
574
615
|
steps: ["status", "add", "commit", "push"],
|
|
@@ -583,7 +624,17 @@ EXEMPLOS DE USO:
|
|
|
583
624
|
completed: true,
|
|
584
625
|
message: "Ciclo completo: arquivos adicionados, commit criado e push realizado" + (organization ? ` [org: ${organization}]` : "")
|
|
585
626
|
}
|
|
586
|
-
}
|
|
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' });
|
|
587
638
|
}
|
|
588
639
|
|
|
589
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"] });
|