@andre.buzeli/git-mcp 15.12.5
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/README.md +40 -0
- package/package.json +29 -0
- package/src/index.js +147 -0
- package/src/prompts/index.js +870 -0
- package/src/providers/providerManager.js +317 -0
- package/src/resources/index.js +276 -0
- package/src/tools/git-branches.js +126 -0
- package/src/tools/git-clone.js +137 -0
- package/src/tools/git-config.js +94 -0
- package/src/tools/git-diff.js +137 -0
- package/src/tools/git-files.js +82 -0
- package/src/tools/git-help.js +284 -0
- package/src/tools/git-history.js +90 -0
- package/src/tools/git-ignore.js +98 -0
- package/src/tools/git-issues.js +101 -0
- package/src/tools/git-merge.js +152 -0
- package/src/tools/git-pulls.js +115 -0
- package/src/tools/git-remote.js +492 -0
- package/src/tools/git-reset.js +105 -0
- package/src/tools/git-stash.js +120 -0
- package/src/tools/git-sync.js +129 -0
- package/src/tools/git-tags.js +113 -0
- package/src/tools/git-workflow.js +443 -0
- package/src/utils/env.js +104 -0
- package/src/utils/errors.js +431 -0
- package/src/utils/gitAdapter.js +996 -0
- package/src/utils/hooks.js +255 -0
- package/src/utils/metrics.js +198 -0
- package/src/utils/providerExec.js +61 -0
- package/src/utils/repoHelpers.js +216 -0
- package/src/utils/retry.js +123 -0
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import Ajv from "ajv";
|
|
2
|
+
import { asToolError, asToolResult, errorToResponse, createError } from "../utils/errors.js";
|
|
3
|
+
import { validateProjectPath } from "../utils/repoHelpers.js";
|
|
4
|
+
|
|
5
|
+
const ajv = new Ajv({ allErrors: true });
|
|
6
|
+
|
|
7
|
+
export function createGitMergeTool(git) {
|
|
8
|
+
const inputSchema = {
|
|
9
|
+
type: "object",
|
|
10
|
+
properties: {
|
|
11
|
+
projectPath: {
|
|
12
|
+
type: "string",
|
|
13
|
+
description: "Caminho absoluto do diretório do projeto"
|
|
14
|
+
},
|
|
15
|
+
action: {
|
|
16
|
+
type: "string",
|
|
17
|
+
enum: ["merge", "abort", "status"],
|
|
18
|
+
description: `Ação a executar:
|
|
19
|
+
- merge: Faz merge de uma branch na branch atual
|
|
20
|
+
- abort: Aborta um merge em andamento (se houver conflitos)
|
|
21
|
+
- status: Verifica se há merge em andamento`
|
|
22
|
+
},
|
|
23
|
+
branch: {
|
|
24
|
+
type: "string",
|
|
25
|
+
description: "Branch para fazer merge na branch atual. Ex: 'feature/nova-funcionalidade'"
|
|
26
|
+
},
|
|
27
|
+
message: {
|
|
28
|
+
type: "string",
|
|
29
|
+
description: "Mensagem do commit de merge. Se não fornecida, usa mensagem padrão"
|
|
30
|
+
},
|
|
31
|
+
noCommit: {
|
|
32
|
+
type: "boolean",
|
|
33
|
+
description: "Se true, faz merge mas não cria commit automaticamente. Default: false"
|
|
34
|
+
},
|
|
35
|
+
squash: {
|
|
36
|
+
type: "boolean",
|
|
37
|
+
description: "Se true, squash all commits into one. Default: false"
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
required: ["projectPath", "action"],
|
|
41
|
+
additionalProperties: false
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const description = `Merge de branches Git.
|
|
45
|
+
|
|
46
|
+
QUANDO USAR:
|
|
47
|
+
- Integrar mudanças de uma feature branch na main
|
|
48
|
+
- Combinar trabalho de diferentes branches
|
|
49
|
+
- Finalizar uma feature/bugfix
|
|
50
|
+
|
|
51
|
+
FLUXO TÍPICO:
|
|
52
|
+
1. git-branches checkout branch='main' → ir para branch destino
|
|
53
|
+
2. git-merge merge branch='feature/x' → fazer merge
|
|
54
|
+
3. git-workflow push → enviar resultado
|
|
55
|
+
|
|
56
|
+
CONFLITOS:
|
|
57
|
+
- Se houver conflitos, o merge será interrompido
|
|
58
|
+
- Resolva os conflitos manualmente
|
|
59
|
+
- Use git-workflow add e commit para finalizar
|
|
60
|
+
- Ou use git-merge abort para cancelar
|
|
61
|
+
|
|
62
|
+
SQUASH:
|
|
63
|
+
- Use squash=true para combinar todos os commits em um só
|
|
64
|
+
- Útil para manter histórico limpo`;
|
|
65
|
+
|
|
66
|
+
async function handle(args) {
|
|
67
|
+
const validate = ajv.compile(inputSchema);
|
|
68
|
+
if (!validate(args || {})) return asToolError("VALIDATION_ERROR", "Parâmetros inválidos", validate.errors);
|
|
69
|
+
const { projectPath, action } = args;
|
|
70
|
+
|
|
71
|
+
try {
|
|
72
|
+
validateProjectPath(projectPath);
|
|
73
|
+
if (action === "status") {
|
|
74
|
+
const mergeStatus = await git.getMergeStatus(projectPath);
|
|
75
|
+
return asToolResult(mergeStatus);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (action === "abort") {
|
|
79
|
+
await git.abortMerge(projectPath);
|
|
80
|
+
return asToolResult({
|
|
81
|
+
success: true,
|
|
82
|
+
message: "Merge abortado com sucesso"
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (action === "merge") {
|
|
87
|
+
if (!args.branch) {
|
|
88
|
+
return asToolError("MISSING_PARAMETER", "branch é obrigatório para merge", {
|
|
89
|
+
parameter: "branch",
|
|
90
|
+
example: "feature/nova-funcionalidade"
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Pre-merge check: working tree status
|
|
95
|
+
const status = await git.status(projectPath);
|
|
96
|
+
if (!status.isClean) {
|
|
97
|
+
// We could warn or fail. Standard git refuses merge if changes would be overwritten.
|
|
98
|
+
// Let's just warn in logs or return error if critical?
|
|
99
|
+
// Git will fail anyway if conflicts with local changes.
|
|
100
|
+
// Let's inform the user.
|
|
101
|
+
console.warn("[GitMerge] Working tree not clean. Merge might fail.");
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const currentBranch = await git.getCurrentBranch(projectPath);
|
|
105
|
+
if (currentBranch === args.branch) {
|
|
106
|
+
return asToolError("VALIDATION_ERROR", "Não pode fazer merge de uma branch nela mesma", {
|
|
107
|
+
branch: args.branch,
|
|
108
|
+
suggestion: "Faça checkout para a branch destino primeiro"
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
try {
|
|
113
|
+
const result = await git.merge(projectPath, args.branch, {
|
|
114
|
+
message: args.message,
|
|
115
|
+
noCommit: args.noCommit,
|
|
116
|
+
squash: args.squash
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
return asToolResult({
|
|
120
|
+
success: true,
|
|
121
|
+
from: args.branch,
|
|
122
|
+
into: currentBranch,
|
|
123
|
+
...result,
|
|
124
|
+
message: result.conflicts?.length > 0
|
|
125
|
+
? `Merge com conflitos. Resolva os conflitos e faça commit.`
|
|
126
|
+
: `Merge de '${args.branch}' em '${currentBranch}' concluído`
|
|
127
|
+
});
|
|
128
|
+
} catch (e) {
|
|
129
|
+
const msg = e.message || "";
|
|
130
|
+
if (msg.includes("conflict") || msg.includes("CONFLICT")) {
|
|
131
|
+
return asToolResult({
|
|
132
|
+
success: false,
|
|
133
|
+
conflict: true,
|
|
134
|
+
message: "Merge resultou em conflitos. Resolva manualmente e faça commit.",
|
|
135
|
+
files: [], // If we could parse files from error, great.
|
|
136
|
+
nextStep: "Edite os arquivos conflitantes, use git add e git commit."
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
throw e;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return asToolError("VALIDATION_ERROR", `Ação '${action}' não suportada`, {
|
|
144
|
+
availableActions: ["merge", "abort", "status"]
|
|
145
|
+
});
|
|
146
|
+
} catch (e) {
|
|
147
|
+
return errorToResponse(e);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return { name: "git-merge", description, inputSchema, handle };
|
|
152
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import Ajv from "ajv";
|
|
2
|
+
import axios from "axios";
|
|
3
|
+
import { asToolError, asToolResult, errorToResponse } from "../utils/errors.js";
|
|
4
|
+
import { getRepoNameFromPath, validateProjectPath } from "../utils/repoHelpers.js";
|
|
5
|
+
import { runBoth } from "../utils/providerExec.js";
|
|
6
|
+
|
|
7
|
+
const ajv = new Ajv({ allErrors: true });
|
|
8
|
+
|
|
9
|
+
export function createGitPullsTool(pm) {
|
|
10
|
+
const inputSchema = {
|
|
11
|
+
type: "object",
|
|
12
|
+
properties: {
|
|
13
|
+
projectPath: {
|
|
14
|
+
type: "string",
|
|
15
|
+
description: "Caminho absoluto do diretório do projeto"
|
|
16
|
+
},
|
|
17
|
+
action: {
|
|
18
|
+
type: "string",
|
|
19
|
+
enum: ["create", "list", "files"],
|
|
20
|
+
description: `Ação a executar:
|
|
21
|
+
- create: Cria Pull Request no GitHub E Gitea
|
|
22
|
+
- list: Lista Pull Requests existentes
|
|
23
|
+
- files: Lista arquivos modificados em um PR`
|
|
24
|
+
},
|
|
25
|
+
title: {
|
|
26
|
+
type: "string",
|
|
27
|
+
description: "Título do PR. Ex: 'feat: adiciona autenticação OAuth'"
|
|
28
|
+
},
|
|
29
|
+
head: {
|
|
30
|
+
type: "string",
|
|
31
|
+
description: "Branch de origem (com as mudanças). Ex: 'feature/oauth'"
|
|
32
|
+
},
|
|
33
|
+
base: {
|
|
34
|
+
type: "string",
|
|
35
|
+
description: "Branch de destino (onde será mergeado). Ex: 'main', 'develop'"
|
|
36
|
+
},
|
|
37
|
+
number: {
|
|
38
|
+
type: "number",
|
|
39
|
+
description: "Número do PR para action='files'"
|
|
40
|
+
},
|
|
41
|
+
body: {
|
|
42
|
+
type: "string",
|
|
43
|
+
description: "Descrição do PR com detalhes das mudanças"
|
|
44
|
+
},
|
|
45
|
+
organization: {
|
|
46
|
+
type: "string",
|
|
47
|
+
description: "Opcional. Nome da organização no GitHub/Gitea. Se informado, operações são feitas na org em vez da conta pessoal."
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
required: ["projectPath", "action"],
|
|
51
|
+
additionalProperties: false
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const description = `Gerenciamento de Pull Requests no GitHub e Gitea.
|
|
55
|
+
|
|
56
|
+
QUANDO USAR:
|
|
57
|
+
- Propor mudanças de uma branch para outra
|
|
58
|
+
- Code review antes de merge
|
|
59
|
+
- Documentar mudanças significativas
|
|
60
|
+
|
|
61
|
+
FLUXO TÍPICO:
|
|
62
|
+
1. Cria branch: git-branches create branch='feature/nova'
|
|
63
|
+
2. Faz mudanças e commits
|
|
64
|
+
3. Push: git-workflow push
|
|
65
|
+
4. Cria PR: git-pulls create head='feature/nova' base='main' title='Nova feature'
|
|
66
|
+
|
|
67
|
+
AÇÕES:
|
|
68
|
+
- create: Criar novo PR
|
|
69
|
+
- list: Ver PRs existentes
|
|
70
|
+
- files: Ver arquivos modificados em um PR
|
|
71
|
+
|
|
72
|
+
NOTA: O PR é criado em AMBOS os providers simultaneamente.`;
|
|
73
|
+
|
|
74
|
+
async function handle(args) {
|
|
75
|
+
const validate = ajv.compile(inputSchema);
|
|
76
|
+
if (!validate(args || {})) return asToolError("VALIDATION_ERROR", "Parâmetros inválidos", validate.errors);
|
|
77
|
+
validateProjectPath(args.projectPath);
|
|
78
|
+
const repo = getRepoNameFromPath(args.projectPath);
|
|
79
|
+
try {
|
|
80
|
+
if (args.action === "create") {
|
|
81
|
+
if (!args.head) return asToolError("MISSING_PARAMETER", "head (branch de origem) é obrigatório", { parameter: "head", example: "feature/nova-funcionalidade" });
|
|
82
|
+
if (!args.base) return asToolError("MISSING_PARAMETER", "base (branch de destino) é obrigatório", { parameter: "base", example: "main" });
|
|
83
|
+
const head = args.head;
|
|
84
|
+
const base = args.base;
|
|
85
|
+
const title = args.title || `${head} -> ${base}`;
|
|
86
|
+
const body = args.body || "";
|
|
87
|
+
const out = await withRetry(() => runBoth(pm, {
|
|
88
|
+
github: async (owner) => { const r = await pm.github.rest.pulls.create({ owner, repo, title, head, base, body }); return { ok: true, number: r.data.number, url: r.data.html_url }; },
|
|
89
|
+
gitea: async (owner) => { const baseUrl = pm.giteaUrl.replace(/\/$/, ""); const r = await axios.post(`${baseUrl}/api/v1/repos/${owner}/${repo}/pulls`, { title, head, base, body }, { headers: { Authorization: `token ${pm.giteaToken}` } }); return { ok: true, number: r.data?.number }; }
|
|
90
|
+
}, { organization: args.organization }), 3, "pulls-create");
|
|
91
|
+
return asToolResult({ success: !!(out.github?.ok || out.gitea?.ok), title, head, base, providers: out });
|
|
92
|
+
}
|
|
93
|
+
if (args.action === "list") {
|
|
94
|
+
const out = await withRetry(() => runBoth(pm, {
|
|
95
|
+
github: async (owner) => { const r = await pm.github.rest.pulls.list({ owner, repo, state: "all", per_page: 30 }); return { ok: true, count: r.data.length, pulls: r.data.slice(0, 10).map(p => ({ number: p.number, title: p.title, state: p.state, head: p.head.ref, base: p.base.ref })) }; },
|
|
96
|
+
gitea: async (owner) => { const baseUrl = pm.giteaUrl.replace(/\/$/, ""); const r = await axios.get(`${baseUrl}/api/v1/repos/${owner}/${repo}/pulls?state=all`, { headers: { Authorization: `token ${pm.giteaToken}` } }); return { ok: true, count: (r.data || []).length, pulls: (r.data || []).slice(0, 10).map(p => ({ number: p.number, title: p.title, state: p.state, head: p.head?.ref, base: p.base?.ref })) }; }
|
|
97
|
+
}, { organization: args.organization }), 3, "pulls-list");
|
|
98
|
+
return asToolResult({ providers: out });
|
|
99
|
+
}
|
|
100
|
+
if (args.action === "files") {
|
|
101
|
+
if (!args.number) return asToolError("MISSING_PARAMETER", "number é obrigatório para ver arquivos do PR", { parameter: "number" });
|
|
102
|
+
const out = await withRetry(() => runBoth(pm, {
|
|
103
|
+
github: async (owner) => { const r = await pm.github.rest.pulls.listFiles({ owner, repo, pull_number: args.number }); return { ok: true, count: r.data.length, files: r.data.map(f => ({ filename: f.filename, status: f.status, additions: f.additions, deletions: f.deletions })) }; },
|
|
104
|
+
gitea: async (owner) => { const baseUrl = pm.giteaUrl.replace(/\/$/, ""); const r = await axios.get(`${baseUrl}/api/v1/repos/${owner}/${repo}/pulls/${args.number}/files`, { headers: { Authorization: `token ${pm.giteaToken}` } }); return { ok: true, count: (r.data || []).length, files: (r.data || []).map(f => ({ filename: f.filename, status: f.status })) }; }
|
|
105
|
+
}, { organization: args.organization }), 3, "pulls-files");
|
|
106
|
+
return asToolResult({ success: true, providers: out });
|
|
107
|
+
}
|
|
108
|
+
return asToolError("VALIDATION_ERROR", `Ação '${args.action}' não suportada`, { availableActions: ["create", "list", "files"] });
|
|
109
|
+
} catch (e) {
|
|
110
|
+
return errorToResponse(e);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return { name: "git-pulls", description, inputSchema, handle };
|
|
115
|
+
}
|