@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.
- package/package.json +32 -29
- package/src/index.js +159 -147
- package/src/providers/providerManager.js +31 -104
- package/src/tools/git-branches.js +13 -3
- package/src/tools/git-clone.js +48 -85
- package/src/tools/git-config.js +13 -3
- package/src/tools/git-diff.js +121 -137
- package/src/tools/git-files.js +13 -3
- package/src/tools/git-help.js +322 -284
- package/src/tools/git-history.js +13 -3
- package/src/tools/git-ignore.js +13 -3
- package/src/tools/git-issues.js +13 -3
- package/src/tools/git-merge.js +13 -3
- package/src/tools/git-pulls.js +14 -4
- package/src/tools/git-remote.js +503 -492
- package/src/tools/git-reset.js +23 -4
- package/src/tools/git-stash.js +13 -3
- package/src/tools/git-sync.js +13 -3
- package/src/tools/git-tags.js +13 -3
- package/src/tools/git-workflow.js +599 -469
- package/src/tools/git-worktree.js +180 -0
- package/src/utils/errors.js +434 -433
- package/src/utils/gitAdapter.js +133 -11
- package/src/utils/mcpNotify.js +45 -0
- package/src/utils/repoHelpers.js +5 -31
- package/src/utils/hooks.js +0 -255
- package/src/utils/metrics.js +0 -198
package/src/utils/gitAdapter.js
CHANGED
|
@@ -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:\\
|
|
14
|
-
"C:\\
|
|
15
|
-
"C:\\
|
|
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
|
-
|
|
617
|
+
// Check if remotes already configured (caller may have set correct fallback URLs)
|
|
620
618
|
const remotesStr = await this._exec(dir, ["remote"]);
|
|
621
|
-
|
|
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
|
-
|
|
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
|
+
}
|
package/src/utils/repoHelpers.js
CHANGED
|
@@ -179,38 +179,12 @@ export function detectProjectType(projectPath) {
|
|
|
179
179
|
}
|
|
180
180
|
|
|
181
181
|
/**
|
|
182
|
-
*
|
|
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
|
-
|
|
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
|
+
|
package/src/utils/hooks.js
DELETED
|
@@ -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
|
-
}
|