@andre.buzeli/git-mcp 16.0.6 → 16.1.2

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.
@@ -7,12 +7,10 @@ import { getRepoNameFromPath, getProvidersEnv, findGitRoot } from "./repoHelpers
7
7
  // Common locations for Git on Windows, Linux, and macOS
8
8
  const GIT_CANDIDATES = [
9
9
  // Windows paths
10
- "C:\\Program Files\\Git\\mingw64\\bin\\git.exe",
11
- "C:\\Program Files\\Git\\bin\\git.exe",
12
10
  "C:\\Program Files\\Git\\cmd\\git.exe",
13
- "C:\\Users\\andre\\AppData\\Local\\Programs\\Git\\mingw64\\bin\\git.exe",
14
- "C:\\Users\\andre\\AppData\\Local\\Programs\\Git\\bin\\git.exe",
15
- "C:\\Users\\andre\\AppData\\Local\\Programs\\Git\\cmd\\git.exe",
11
+ "C:\\Program Files\\Git\\bin\\git.exe",
12
+ "C:\\Program Files\\Git\\mingw64\\bin\\git.exe",
13
+ "C:\\Program Files (x86)\\Git\\cmd\\git.exe",
16
14
  // Linux/macOS paths
17
15
  "/usr/bin/git",
18
16
  "/usr/local/bin/git",
@@ -616,9 +614,16 @@ export class GitAdapter {
616
614
  }
617
615
 
618
616
  async pushParallel(dir, branch, force = false, organization) {
619
- await this.ensureRemotes(dir, { organization });
617
+ // Check if remotes already configured (caller may have set correct fallback URLs)
620
618
  const remotesStr = await this._exec(dir, ["remote"]);
621
- const remotes = remotesStr.split("\n").filter(r => ["github", "gitea"].includes(r.trim()));
619
+ let remotes = remotesStr.split("\n").filter(r => ["github", "gitea"].includes(r.trim()));
620
+
621
+ // Only setup remotes if none exist — caller is responsible for correct URLs
622
+ if (remotes.length === 0) {
623
+ await this.ensureRemotes(dir, { organization });
624
+ const newStr = await this._exec(dir, ["remote"]);
625
+ remotes = newStr.split("\n").filter(r => ["github", "gitea"].includes(r.trim()));
626
+ }
622
627
 
623
628
  if (remotes.length === 0) {
624
629
  throw createError("REMOTE_NOT_FOUND", { message: "Nenhum remote github/gitea configurado" });
@@ -643,10 +648,13 @@ export class GitAdapter {
643
648
  try {
644
649
  console.error(`[GitAdapter] Auto-fix: repositório não existe no remote '${remote}', criando automaticamente...`);
645
650
  const repoName = getRepoNameFromPath(dir);
646
- const ensured = await this.pm.ensureRepos({ repoName, createIfMissing: true, isPublic: false });
651
+ const ensured = await this.pm.ensureRepos({ repoName, createIfMissing: true, isPublic: false, organization });
647
652
 
648
- // Atualiza remotes após criar repo
649
- await this.ensureRemotes(dir, {});
653
+ // Atualiza remotes após criar repo — use actual result URLs
654
+ const ghUrl = ensured.github?.ok && ensured.github.repo ? `https://github.com/${ensured.github.repo}.git` : "";
655
+ const gtBase = this.pm.giteaUrl?.replace(/\/$/, "");
656
+ const gtUrl = ensured.gitea?.ok && ensured.gitea.repo && gtBase ? `${gtBase}/${ensured.gitea.repo}.git` : "";
657
+ await this.ensureRemotes(dir, { githubUrl: ghUrl, giteaUrl: gtUrl, organization });
650
658
 
651
659
  // Tenta push novamente
652
660
  await this.pushOne(dir, remote, branch, force, true); // Usa -u para criar branch também
@@ -953,7 +961,6 @@ export class GitAdapter {
953
961
  // ============ DIFF/CLONE ============
954
962
  async diff(dir, options = {}) { return await this._exec(dir, ["diff"]); }
955
963
  async diffCommits(dir, from, to) { return await this._exec(dir, ["diff", from, to]); }
956
- async diffStats(dir, from, to) { const out = await this._exec(dir, ["diff", "--stat", from, to]); return { message: out }; }
957
964
 
958
965
  async clone(url, dir, options = {}) {
959
966
  const { branch, depth, singleBranch } = options;
@@ -1022,6 +1029,121 @@ export class GitAdapter {
1022
1029
  return Array.from(map.values());
1023
1030
  }
1024
1031
 
1032
+ // ============ WORKTREE ============
1033
+
1034
+ async addWorktree(dir, branch, worktreePath) {
1035
+ // Verifica se branch já existe
1036
+ const branches = await this.listBranches(dir);
1037
+ const branchExists = branches.includes(branch);
1038
+
1039
+ const args = ["worktree", "add"];
1040
+ // Se branch não existe, cria com -b. Se existe, usa checkout normal (sem flag)
1041
+ if (!branchExists) {
1042
+ args.push("-b", branch);
1043
+ }
1044
+ args.push(worktreePath, branch);
1045
+
1046
+ await this._exec(dir, args);
1047
+ }
1048
+
1049
+ async listWorktrees(dir) {
1050
+ try {
1051
+ const out = await this._exec(dir, ["worktree", "list", "--porcelain"]);
1052
+ const worktrees = [];
1053
+ let current = {};
1054
+
1055
+ const lines = out.split("\n");
1056
+ for (const line of lines) {
1057
+ if (!line.trim()) {
1058
+ if (current.worktree) {
1059
+ worktrees.push(current);
1060
+ current = {};
1061
+ }
1062
+ continue;
1063
+ }
1064
+
1065
+ const [key, ...rest] = line.split(" ");
1066
+ const value = rest.join(" ");
1067
+
1068
+ if (key === "worktree") {
1069
+ if (current.worktree) worktrees.push(current);
1070
+ current = { worktree: value };
1071
+ } else if (key === "branch") {
1072
+ current.branch = value.replace("refs/heads/", "");
1073
+ } else if (key === "HEAD") {
1074
+ current.head = value;
1075
+ }
1076
+ }
1077
+ if (current.worktree) worktrees.push(current);
1078
+
1079
+ return worktrees.map(w => ({
1080
+ path: w.worktree,
1081
+ branch: w.branch,
1082
+ head: w.head
1083
+ }));
1084
+ } catch (e) {
1085
+ // Fallback para versão antiga do git se --porcelain falhar
1086
+ const out = await this._exec(dir, ["worktree", "list"]);
1087
+ return out.split("\n").filter(Boolean).map(line => {
1088
+ const parts = line.split(/\s+/);
1089
+ const path = parts.slice(0, -2).join(" "); // Path pode ter espaços
1090
+ const head = parts[parts.length - 2];
1091
+ const branchRaw = parts[parts.length - 1];
1092
+ const branch = branchRaw.replace("[", "").replace("]", "");
1093
+ return { path, head, branch };
1094
+ });
1095
+ }
1096
+ }
1097
+
1098
+ async removeWorktree(dir, worktreePath) {
1099
+ await this._exec(dir, ["worktree", "remove", worktreePath, "--force"]);
1100
+ }
1101
+
1102
+ async pushRefspec(dir, remote, localBranch, remoteBranch, force = false) {
1103
+ const remoteUrl = await this._exec(dir, ["remote", "get-url", remote]);
1104
+ const header = this._getAuthHeader(remoteUrl);
1105
+ const args = [];
1106
+ if (header) args.push("-c", `http.extraHeader=${header}`);
1107
+ args.push("push");
1108
+ if (force) args.push("--force");
1109
+ args.push(remote, `${localBranch}:${remoteBranch}`);
1110
+ await this._exec(dir, args);
1111
+ }
1112
+
1113
+ async getWorktreeConfigs(dir) {
1114
+ try {
1115
+ const configPath = path.join(dir, ".git", "config");
1116
+ if (!fs.existsSync(configPath)) return [];
1117
+
1118
+ const content = fs.readFileSync(configPath, "utf8");
1119
+ const results = [];
1120
+ const sectionRegex = /\[worktree-branch "([^"]+)"\]([\s\S]*?)(?=\[|$)/g;
1121
+
1122
+ let match;
1123
+ while ((match = sectionRegex.exec(content)) !== null) {
1124
+ const branchName = match[1];
1125
+ const body = match[2];
1126
+ const wtPath = body.match(/path\s*=\s*(.+)/)?.[1]?.trim();
1127
+ const channel = body.match(/channel\s*=\s*(.+)/)?.[1]?.trim() || "production";
1128
+ if (branchName && wtPath) {
1129
+ results.push({ branch: branchName, path: wtPath, channel });
1130
+ }
1131
+ }
1132
+ return results;
1133
+ } catch (e) {
1134
+ console.error("[GitAdapter] Error reading worktree configs:", e);
1135
+ return [];
1136
+ }
1137
+ }
1138
+
1139
+ async setWorktreeConfig(dir, branchName, config) {
1140
+ // Normalizar path para usar forward slashes (compatível com git config em windows)
1141
+ const normalizedPath = config.path.replace(/\\/g, "/");
1142
+
1143
+ await this._exec(dir, ["config", `worktree-branch.${branchName}.path`, normalizedPath]);
1144
+ await this._exec(dir, ["config", `worktree-branch.${branchName}.channel`, config.channel || "production"]);
1145
+ }
1146
+
1025
1147
  // ============ GIT LFS SUPPORT ============
1026
1148
 
1027
1149
  /**
@@ -0,0 +1,45 @@
1
+ // Helpers para notificações MCP com graceful degradation
2
+ // Permite que o server funcione mesmo se o IDE não suportar features avançadas
3
+
4
+ // sendProgress - envia notificacao de progresso
5
+ export async function sendProgress(server, token, progress, total, message) {
6
+ if (!token) return;
7
+ try {
8
+ await server.notification({
9
+ method: "notifications/progress",
10
+ params: { progressToken: token, progress, total, message }
11
+ });
12
+ } catch (e) { /* IDE nao suporta — ignora */ }
13
+ }
14
+
15
+ // sendLog - envia log estruturado
16
+ export async function sendLog(server, level, message, data) {
17
+ try {
18
+ await server.notification({
19
+ method: "notifications/message",
20
+ params: { level, logger: "git-mcp", data: data || message }
21
+ });
22
+ } catch (e) { /* IDE nao suporta — ignora */ }
23
+ }
24
+
25
+ // requestConfirmation - pede confirmacao via elicitation
26
+ export async function requestConfirmation(server, message) {
27
+ try {
28
+ const result = await server.request(
29
+ { method: "elicitation/create", params: {
30
+ mode: "form",
31
+ message,
32
+ requestedSchema: {
33
+ type: "object",
34
+ properties: { confirm: { type: "string", enum: ["yes", "no"], title: "Confirmar?" } },
35
+ required: ["confirm"]
36
+ }
37
+ }},
38
+ /* schema de validacao opcional */
39
+ );
40
+ if (result.action === "accept" && result.content?.confirm === "yes") return true;
41
+ return false;
42
+ } catch (e) {
43
+ return true; // IDE nao suporta elicitation — prossegue sem confirmar (fail-open)
44
+ }
45
+ }
@@ -179,38 +179,12 @@ export function detectProjectType(projectPath) {
179
179
  }
180
180
 
181
181
  /**
182
- * Executa uma operação com retries exponenciais
183
- * @param {Function} operation - Função assíncrona a executar
184
- * @param {number} maxRetries - Número máximo de tentativas
185
- * @param {string} context - Contexto para logs
186
- * @returns {Promise<any>} Resultado da operação
182
+ * Re-exporta withRetry de retry.js com adaptador para API legada (fn, maxRetries, context)
187
183
  */
184
+ import { withRetry as _withRetry } from "./retry.js";
185
+
188
186
  export async function withRetry(operation, maxRetries = 3, context = "") {
189
- let lastError;
190
- for (let i = 0; i < maxRetries; i++) {
191
- try {
192
- return await operation();
193
- } catch (e) {
194
- lastError = e;
195
- const msg = e.message || String(e);
196
- // Retry on network errors, lock files, or timeouts
197
- const isRetryable = msg.includes("lock") ||
198
- msg.includes("network") ||
199
- msg.includes("resolve host") ||
200
- msg.includes("timeout") ||
201
- msg.includes("connection") ||
202
- msg.includes("ECONNRESET") ||
203
- msg.includes("ETIMEDOUT");
204
-
205
- if (!isRetryable && i === 0) throw e; // Fail fast if not retryable and first attempt
206
-
207
- if (i < maxRetries - 1) {
208
- const delay = 2000 * Math.pow(2, i);
209
- console.warn(`[${context}] Attempt ${i + 1} failed, retrying in ${delay}ms... Error: ${msg}`);
210
- await new Promise(r => setTimeout(r, delay));
211
- }
212
- }
213
- }
214
- throw lastError;
187
+ return _withRetry(operation, { maxRetries });
215
188
  }
216
189
 
190
+
@@ -1,255 +0,0 @@
1
- // Sistema de Hooks para git-mcp
2
- // Permite executar código customizado antes/depois de operações
3
-
4
- /**
5
- * Tipos de hooks disponíveis
6
- */
7
- export const HOOK_TYPES = {
8
- // Workflow hooks
9
- PRE_INIT: "pre:init",
10
- POST_INIT: "post:init",
11
- PRE_COMMIT: "pre:commit",
12
- POST_COMMIT: "post:commit",
13
- PRE_PUSH: "pre:push",
14
- POST_PUSH: "post:push",
15
- PRE_PULL: "pre:pull",
16
- POST_PULL: "post:pull",
17
-
18
- // Branch hooks
19
- PRE_BRANCH_CREATE: "pre:branch:create",
20
- POST_BRANCH_CREATE: "post:branch:create",
21
- PRE_BRANCH_DELETE: "pre:branch:delete",
22
- POST_BRANCH_DELETE: "post:branch:delete",
23
- PRE_CHECKOUT: "pre:checkout",
24
- POST_CHECKOUT: "post:checkout",
25
-
26
- // Merge hooks
27
- PRE_MERGE: "pre:merge",
28
- POST_MERGE: "post:merge",
29
- ON_CONFLICT: "on:conflict",
30
-
31
- // Reset hooks
32
- PRE_RESET: "pre:reset",
33
- POST_RESET: "post:reset",
34
-
35
- // Remote hooks
36
- PRE_SYNC: "pre:sync",
37
- POST_SYNC: "post:sync",
38
-
39
- // Error handling
40
- ON_ERROR: "on:error"
41
- };
42
-
43
- // Armazena hooks registrados
44
- const registeredHooks = new Map();
45
-
46
- /**
47
- * Registra um hook
48
- * @param {string} hookType - Tipo do hook (usar HOOK_TYPES)
49
- * @param {Function} handler - Função async (context) => result
50
- * @param {Object} options - { priority: number, name: string }
51
- * @returns {string} - ID do hook para remover depois
52
- */
53
- export function registerHook(hookType, handler, options = {}) {
54
- const { priority = 0, name = "anonymous" } = options;
55
- const hookId = `${hookType}_${name}_${Date.now()}`;
56
-
57
- if (!registeredHooks.has(hookType)) {
58
- registeredHooks.set(hookType, []);
59
- }
60
-
61
- registeredHooks.get(hookType).push({
62
- id: hookId,
63
- handler,
64
- priority,
65
- name
66
- });
67
-
68
- // Ordena por prioridade (maior primeiro)
69
- registeredHooks.get(hookType).sort((a, b) => b.priority - a.priority);
70
-
71
- return hookId;
72
- }
73
-
74
- /**
75
- * Remove um hook específico
76
- * @param {string} hookId - ID retornado por registerHook
77
- */
78
- export function unregisterHook(hookId) {
79
- for (const [type, hooks] of registeredHooks.entries()) {
80
- const index = hooks.findIndex(h => h.id === hookId);
81
- if (index !== -1) {
82
- hooks.splice(index, 1);
83
- return true;
84
- }
85
- }
86
- return false;
87
- }
88
-
89
- /**
90
- * Remove todos os hooks de um tipo
91
- * @param {string} hookType - Tipo do hook
92
- */
93
- export function clearHooks(hookType) {
94
- if (hookType) {
95
- registeredHooks.delete(hookType);
96
- } else {
97
- registeredHooks.clear();
98
- }
99
- }
100
-
101
- /**
102
- * Executa todos os hooks de um tipo
103
- * @param {string} hookType - Tipo do hook
104
- * @param {Object} context - Contexto passado para os handlers
105
- * @returns {Object} - { success: boolean, results: [], errors: [] }
106
- */
107
- export async function runHooks(hookType, context = {}) {
108
- const hooks = registeredHooks.get(hookType) || [];
109
-
110
- if (hooks.length === 0) {
111
- return { success: true, results: [], errors: [], skipped: true };
112
- }
113
-
114
- const results = [];
115
- const errors = [];
116
- let shouldContinue = true;
117
-
118
- for (const hook of hooks) {
119
- if (!shouldContinue) break;
120
-
121
- try {
122
- const result = await hook.handler({
123
- ...context,
124
- hookType,
125
- hookName: hook.name
126
- });
127
-
128
- results.push({
129
- hookId: hook.id,
130
- name: hook.name,
131
- success: true,
132
- result
133
- });
134
-
135
- // Hook pode retornar { abort: true } para parar execução
136
- if (result?.abort) {
137
- shouldContinue = false;
138
- }
139
- } catch (error) {
140
- errors.push({
141
- hookId: hook.id,
142
- name: hook.name,
143
- error: error.message || String(error)
144
- });
145
-
146
- // Por padrão, continua mesmo com erro
147
- // Hook pode definir { stopOnError: true } nas options
148
- }
149
- }
150
-
151
- return {
152
- success: errors.length === 0,
153
- results,
154
- errors,
155
- aborted: !shouldContinue
156
- };
157
- }
158
-
159
- /**
160
- * Wrapper para executar função com hooks pre/post
161
- * @param {string} operationType - Tipo da operação (ex: "commit", "push")
162
- * @param {Function} fn - Função principal async
163
- * @param {Object} context - Contexto para os hooks
164
- */
165
- export async function withHooks(operationType, fn, context = {}) {
166
- const preHookType = `pre:${operationType}`;
167
- const postHookType = `post:${operationType}`;
168
-
169
- // Pre-hooks
170
- const preResult = await runHooks(preHookType, context);
171
- if (preResult.aborted) {
172
- return {
173
- success: false,
174
- abortedByHook: true,
175
- hookResults: preResult,
176
- message: `Operação abortada por hook ${preHookType}`
177
- };
178
- }
179
-
180
- // Operação principal
181
- let mainResult;
182
- let mainError = null;
183
-
184
- try {
185
- mainResult = await fn();
186
- } catch (error) {
187
- mainError = error;
188
-
189
- // Executa hook de erro
190
- await runHooks(HOOK_TYPES.ON_ERROR, {
191
- ...context,
192
- error: {
193
- message: error.message,
194
- code: error.code,
195
- stack: error.stack
196
- }
197
- });
198
-
199
- throw error;
200
- }
201
-
202
- // Post-hooks
203
- const postResult = await runHooks(postHookType, {
204
- ...context,
205
- result: mainResult
206
- });
207
-
208
- return mainResult;
209
- }
210
-
211
- /**
212
- * Lista todos os hooks registrados
213
- */
214
- export function listHooks() {
215
- const list = {};
216
- for (const [type, hooks] of registeredHooks.entries()) {
217
- list[type] = hooks.map(h => ({
218
- id: h.id,
219
- name: h.name,
220
- priority: h.priority
221
- }));
222
- }
223
- return list;
224
- }
225
-
226
- /**
227
- * Verifica se há hooks registrados para um tipo
228
- */
229
- export function hasHooks(hookType) {
230
- const hooks = registeredHooks.get(hookType);
231
- return hooks && hooks.length > 0;
232
- }
233
-
234
- /**
235
- * Exemplo de registro de hooks
236
- */
237
- export function exampleHookSetup() {
238
- // Hook para logging
239
- registerHook(HOOK_TYPES.POST_COMMIT, async (ctx) => {
240
- console.log(`[Hook] Commit criado: ${ctx.result?.sha}`);
241
- }, { name: "commit-logger", priority: 10 });
242
-
243
- // Hook para validação pré-push
244
- registerHook(HOOK_TYPES.PRE_PUSH, async (ctx) => {
245
- // Exemplo: verificar se branch é protegida
246
- if (ctx.branch === "main" && !ctx.force) {
247
- console.warn("[Hook] Push para main sem force - considere criar PR");
248
- }
249
- }, { name: "branch-protection", priority: 100 });
250
-
251
- // Hook para notificação de erro
252
- registerHook(HOOK_TYPES.ON_ERROR, async (ctx) => {
253
- console.error(`[Hook] Erro: ${ctx.error?.message}`);
254
- }, { name: "error-notifier", priority: 0 });
255
- }