@andrebuzeli/git-mcp 15.9.2 → 15.10.0
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 +1 -1
- package/src/tools/git-branches.js +19 -1
- package/src/tools/git-clone.js +39 -6
- package/src/tools/git-config.js +4 -4
- package/src/tools/git-diff.js +6 -6
- package/src/tools/git-files.js +1 -1
- package/src/tools/git-history.js +2 -2
- package/src/tools/git-ignore.js +4 -4
- package/src/tools/git-issues.js +7 -7
- package/src/tools/git-merge.js +39 -15
- package/src/tools/git-pulls.js +7 -7
- package/src/tools/git-remote.js +1 -1
- package/src/tools/git-reset.js +5 -5
- package/src/tools/git-stash.js +4 -4
- package/src/tools/git-sync.js +39 -4
- package/src/tools/git-tags.js +5 -5
- package/src/tools/git-workflow.js +10 -2
- package/src/utils/gitAdapter.js +1 -1
- package/src/utils/repoHelpers.js +56 -0
package/package.json
CHANGED
|
@@ -95,7 +95,25 @@ CONVENÇÕES DE NOMES:
|
|
|
95
95
|
}
|
|
96
96
|
if (action === "checkout") {
|
|
97
97
|
if (!args.branch) return asToolError("MISSING_PARAMETER", "branch é obrigatório para checkout", { parameter: "branch" });
|
|
98
|
-
|
|
98
|
+
|
|
99
|
+
try {
|
|
100
|
+
await git.checkout(projectPath, args.branch);
|
|
101
|
+
} catch (e) {
|
|
102
|
+
const msg = e.message || "";
|
|
103
|
+
// Auto-fix: Create if not exists (common intent)
|
|
104
|
+
if (msg.includes("did not match any file") || msg.includes("pathspec") || msg.includes("not found")) {
|
|
105
|
+
// We can suggest or auto-create if configured. For now, let's suggest clearly or auto-create if it looks like a feature branch?
|
|
106
|
+
// Safer: Suggest. Or better: check if we should auto-create.
|
|
107
|
+
// Let's stick to safe behavior but enhanced error.
|
|
108
|
+
|
|
109
|
+
// Check if user meant create
|
|
110
|
+
return asToolError("BRANCH_NOT_FOUND", `Branch '${args.branch}' não existe`, {
|
|
111
|
+
suggestion: "Use action='create' para criar esta branch, ou verifique o nome."
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
throw e;
|
|
115
|
+
}
|
|
116
|
+
|
|
99
117
|
return asToolResult({ success: true, branch: args.branch, message: `Mudou para branch '${args.branch}'` });
|
|
100
118
|
}
|
|
101
119
|
return asToolError("VALIDATION_ERROR", `Ação '${action}' não suportada`, { availableActions: ["list", "create", "delete", "rename", "checkout"] });
|
package/src/tools/git-clone.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import Ajv from "ajv";
|
|
2
2
|
import { asToolError, asToolResult, errorToResponse, createError } from "../utils/errors.js";
|
|
3
|
-
import { validateProjectPath } from "../utils/repoHelpers.js";
|
|
3
|
+
import { validateProjectPath, withRetry } from "../utils/repoHelpers.js";
|
|
4
4
|
|
|
5
5
|
const ajv = new Ajv({ allErrors: true });
|
|
6
6
|
|
|
@@ -75,11 +75,44 @@ NOTAS:
|
|
|
75
75
|
});
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
78
|
+
// Idempotency check
|
|
79
|
+
if (fs.existsSync(path.join(projectPath, ".git"))) {
|
|
80
|
+
try {
|
|
81
|
+
// Check if it's the same repo
|
|
82
|
+
// git.getRemoteUrl is not standard in adapter, use exec or listRemotes logic
|
|
83
|
+
// Assuming git.listRemotes or similar exists, or we catch the error
|
|
84
|
+
// Let's try to infer from config or just warn
|
|
85
|
+
|
|
86
|
+
// We'll rely on git status to check if it's healthy
|
|
87
|
+
await git.status(projectPath);
|
|
88
|
+
|
|
89
|
+
return asToolResult({
|
|
90
|
+
success: true,
|
|
91
|
+
url,
|
|
92
|
+
path: projectPath,
|
|
93
|
+
branch: args.branch || "current",
|
|
94
|
+
message: `Repositório já existe em '${projectPath}'. Clone ignorado (idempotente).`,
|
|
95
|
+
nextStep: "Use git-workflow status para ver o estado atual"
|
|
96
|
+
});
|
|
97
|
+
} catch (e) {
|
|
98
|
+
// If status fails, maybe it's broken
|
|
99
|
+
console.warn("Existing repo check failed:", e);
|
|
100
|
+
}
|
|
101
|
+
} else if (fs.existsSync(projectPath) && fs.readdirSync(projectPath).length > 0) {
|
|
102
|
+
return asToolError("DIR_NOT_EMPTY", `Diretório '${projectPath}' existe e não está vazio`, {
|
|
103
|
+
suggestion: "Use um diretório novo ou limpe o atual"
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const result = await withRetry(
|
|
108
|
+
() => git.clone(url, projectPath, {
|
|
109
|
+
branch: args.branch,
|
|
110
|
+
depth: args.depth,
|
|
111
|
+
singleBranch: args.singleBranch
|
|
112
|
+
}),
|
|
113
|
+
3,
|
|
114
|
+
"clone"
|
|
115
|
+
);
|
|
83
116
|
|
|
84
117
|
return asToolResult({
|
|
85
118
|
success: true,
|
package/src/tools/git-config.js
CHANGED
|
@@ -66,21 +66,21 @@ EXEMPLOS:
|
|
|
66
66
|
validateProjectPath(projectPath);
|
|
67
67
|
if (action === "get") {
|
|
68
68
|
if (!args.key) return asToolError("MISSING_PARAMETER", "key é obrigatório", { parameter: "key" });
|
|
69
|
-
const val = await git.getConfig(projectPath, args.key, scope);
|
|
69
|
+
const val = await withRetry(() => git.getConfig(projectPath, args.key, scope), 3, "get-config");
|
|
70
70
|
return asToolResult({ key: args.key, value: val, found: val !== undefined, scope });
|
|
71
71
|
}
|
|
72
72
|
if (action === "set") {
|
|
73
73
|
if (!args.key) return asToolError("MISSING_PARAMETER", "key é obrigatório", { parameter: "key" });
|
|
74
|
-
await git.setConfig(projectPath, args.key, args.value ?? "", scope);
|
|
74
|
+
await withRetry(() => git.setConfig(projectPath, args.key, args.value ?? "", scope), 3, "set-config");
|
|
75
75
|
return asToolResult({ success: true, key: args.key, value: args.value ?? "", scope });
|
|
76
76
|
}
|
|
77
77
|
if (action === "unset") {
|
|
78
78
|
if (!args.key) return asToolError("MISSING_PARAMETER", "key é obrigatório", { parameter: "key" });
|
|
79
|
-
await git.unsetConfig(projectPath, args.key, scope);
|
|
79
|
+
await withRetry(() => git.unsetConfig(projectPath, args.key, scope), 3, "unset-config");
|
|
80
80
|
return asToolResult({ success: true, key: args.key, removed: true, scope });
|
|
81
81
|
}
|
|
82
82
|
if (action === "list") {
|
|
83
|
-
const items = await git.listConfig(projectPath, scope);
|
|
83
|
+
const items = await withRetry(() => git.listConfig(projectPath, scope), 3, "list-config");
|
|
84
84
|
return asToolResult({ scope, configs: items, count: Object.keys(items).length });
|
|
85
85
|
}
|
|
86
86
|
return asToolError("VALIDATION_ERROR", `Ação '${action}' não suportada`, { availableActions: ["get", "set", "unset", "list"] });
|
package/src/tools/git-diff.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import Ajv from "ajv";
|
|
2
2
|
import { asToolError, asToolResult, errorToResponse } from "../utils/errors.js";
|
|
3
|
-
import { validateProjectPath } from "../utils/repoHelpers.js";
|
|
3
|
+
import { validateProjectPath, withRetry } from "../utils/repoHelpers.js";
|
|
4
4
|
|
|
5
5
|
const ajv = new Ajv({ allErrors: true });
|
|
6
6
|
|
|
@@ -80,10 +80,10 @@ EXEMPLOS:
|
|
|
80
80
|
try {
|
|
81
81
|
validateProjectPath(projectPath);
|
|
82
82
|
if (action === "show") {
|
|
83
|
-
const diff = await git.diff(projectPath, {
|
|
83
|
+
const diff = await withRetry(() => git.diff(projectPath, {
|
|
84
84
|
file: fileParam,
|
|
85
85
|
context: args.context || 3
|
|
86
|
-
});
|
|
86
|
+
}), 3, "diff-show");
|
|
87
87
|
|
|
88
88
|
if (!diff || diff.length === 0) {
|
|
89
89
|
return asToolResult({
|
|
@@ -100,10 +100,10 @@ EXEMPLOS:
|
|
|
100
100
|
}
|
|
101
101
|
|
|
102
102
|
if (action === "compare") {
|
|
103
|
-
const diff = await git.diffCommits(projectPath, fromParam, toParam, {
|
|
103
|
+
const diff = await withRetry(() => git.diffCommits(projectPath, fromParam, toParam, {
|
|
104
104
|
file: fileParam,
|
|
105
105
|
context: args.context || 3
|
|
106
|
-
});
|
|
106
|
+
}), 3, "diff-compare");
|
|
107
107
|
|
|
108
108
|
return asToolResult({
|
|
109
109
|
from: fromParam,
|
|
@@ -115,7 +115,7 @@ EXEMPLOS:
|
|
|
115
115
|
}
|
|
116
116
|
|
|
117
117
|
if (action === "stat") {
|
|
118
|
-
const stats = await git.diffStats(projectPath, fromParam, toParam);
|
|
118
|
+
const stats = await withRetry(() => git.diffStats(projectPath, fromParam, toParam), 3, "diff-stat");
|
|
119
119
|
|
|
120
120
|
return asToolResult({
|
|
121
121
|
from: fromParam,
|
package/src/tools/git-files.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import Ajv from "ajv";
|
|
2
2
|
import { asToolError, asToolResult, errorToResponse } from "../utils/errors.js";
|
|
3
|
-
import { validateProjectPath } from "../utils/repoHelpers.js";
|
|
3
|
+
import { validateProjectPath, withRetry } from "../utils/repoHelpers.js";
|
|
4
4
|
|
|
5
5
|
const ajv = new Ajv({ allErrors: true });
|
|
6
6
|
|
package/src/tools/git-history.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import Ajv from "ajv";
|
|
2
2
|
import { asToolError, asToolResult, errorToResponse } from "../utils/errors.js";
|
|
3
|
-
import { validateProjectPath } from "../utils/repoHelpers.js";
|
|
3
|
+
import { validateProjectPath, withRetry } from "../utils/repoHelpers.js";
|
|
4
4
|
|
|
5
5
|
const ajv = new Ajv({ allErrors: true });
|
|
6
6
|
|
|
@@ -58,7 +58,7 @@ EXEMPLOS:
|
|
|
58
58
|
try {
|
|
59
59
|
validateProjectPath(projectPath);
|
|
60
60
|
if (action === "log") {
|
|
61
|
-
const items = await git.log(projectPath, { ref: args.ref || "HEAD", maxCount: args.maxCount || 50 });
|
|
61
|
+
const items = await withRetry(() => git.log(projectPath, { ref: args.ref || "HEAD", maxCount: args.maxCount || 50 }), 3, "history-log");
|
|
62
62
|
if (items.length === 0) {
|
|
63
63
|
return asToolResult({
|
|
64
64
|
commits: [],
|
package/src/tools/git-ignore.js
CHANGED
|
@@ -61,7 +61,7 @@ EXEMPLOS:
|
|
|
61
61
|
try {
|
|
62
62
|
validateProjectPath(projectPath);
|
|
63
63
|
if (action === "list") {
|
|
64
|
-
const items = await git.listGitignore(projectPath);
|
|
64
|
+
const items = await withRetry(() => git.listGitignore(projectPath), 3, "ignore-list");
|
|
65
65
|
return asToolResult({ patterns: items, count: items.length, hasGitignore: items.length > 0 || true });
|
|
66
66
|
}
|
|
67
67
|
if (action === "create") {
|
|
@@ -71,21 +71,21 @@ EXEMPLOS:
|
|
|
71
71
|
suggestion: "Forneça um array de padrões. Ex: ['node_modules/', '*.log', '.env']"
|
|
72
72
|
});
|
|
73
73
|
}
|
|
74
|
-
await git.createGitignore(projectPath, patterns);
|
|
74
|
+
await withRetry(() => git.createGitignore(projectPath, patterns), 3, "ignore-create");
|
|
75
75
|
return asToolResult({ success: true, patterns, message: ".gitignore criado" });
|
|
76
76
|
}
|
|
77
77
|
if (action === "add") {
|
|
78
78
|
if (patterns.length === 0) {
|
|
79
79
|
return asToolError("MISSING_PARAMETER", "patterns é obrigatório", { parameter: "patterns" });
|
|
80
80
|
}
|
|
81
|
-
await git.addToGitignore(projectPath, patterns);
|
|
81
|
+
await withRetry(() => git.addToGitignore(projectPath, patterns), 3, "ignore-add");
|
|
82
82
|
return asToolResult({ success: true, added: patterns });
|
|
83
83
|
}
|
|
84
84
|
if (action === "remove") {
|
|
85
85
|
if (patterns.length === 0) {
|
|
86
86
|
return asToolError("MISSING_PARAMETER", "patterns é obrigatório", { parameter: "patterns" });
|
|
87
87
|
}
|
|
88
|
-
await git.removeFromGitignore(projectPath, patterns);
|
|
88
|
+
await withRetry(() => git.removeFromGitignore(projectPath, patterns), 3, "ignore-remove");
|
|
89
89
|
return asToolResult({ success: true, removed: patterns });
|
|
90
90
|
}
|
|
91
91
|
return asToolError("VALIDATION_ERROR", `Ação '${action}' não suportada`, { availableActions: ["list", "create", "add", "remove"] });
|
package/src/tools/git-issues.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import Ajv from "ajv";
|
|
2
2
|
import axios from "axios";
|
|
3
3
|
import { asToolError, asToolResult, errorToResponse } from "../utils/errors.js";
|
|
4
|
-
import { getRepoNameFromPath, validateProjectPath } from "../utils/repoHelpers.js";
|
|
4
|
+
import { getRepoNameFromPath, validateProjectPath, withRetry } from "../utils/repoHelpers.js";
|
|
5
5
|
import { runBoth } from "../utils/providerExec.js";
|
|
6
6
|
|
|
7
7
|
const ajv = new Ajv({ allErrors: true });
|
|
@@ -65,26 +65,26 @@ BOAS PRÁTICAS:
|
|
|
65
65
|
try {
|
|
66
66
|
if (args.action === "create") {
|
|
67
67
|
if (!args.title) return asToolError("MISSING_PARAMETER", "title é obrigatório para criar issue", { parameter: "title" });
|
|
68
|
-
const out = await runBoth(pm, {
|
|
68
|
+
const out = await withRetry(() => runBoth(pm, {
|
|
69
69
|
github: async (owner) => { const r = await pm.github.rest.issues.create({ owner, repo, title: args.title, body: args.body || "" }); return { ok: true, number: r.data.number, url: r.data.html_url }; },
|
|
70
70
|
gitea: async (owner) => { const base = pm.giteaUrl.replace(/\/$/, ""); const r = await axios.post(`${base}/api/v1/repos/${owner}/${repo}/issues`, { title: args.title, body: args.body || "" }, { headers: { Authorization: `token ${pm.giteaToken}` } }); return { ok: true, number: r.data?.number }; }
|
|
71
|
-
});
|
|
71
|
+
}), 3, "issues-create");
|
|
72
72
|
return asToolResult({ success: !!(out.github?.ok || out.gitea?.ok), title: args.title, providers: out });
|
|
73
73
|
}
|
|
74
74
|
if (args.action === "list") {
|
|
75
|
-
const out = await runBoth(pm, {
|
|
75
|
+
const out = await withRetry(() => runBoth(pm, {
|
|
76
76
|
github: async (owner) => { const r = await pm.github.rest.issues.listForRepo({ owner, repo, state: "all", per_page: 30 }); return { ok: true, count: r.data.length, issues: r.data.slice(0, 10).map(i => ({ number: i.number, title: i.title, state: i.state })) }; },
|
|
77
77
|
gitea: async (owner) => { const base = pm.giteaUrl.replace(/\/$/, ""); const r = await axios.get(`${base}/api/v1/repos/${owner}/${repo}/issues?state=all`, { headers: { Authorization: `token ${pm.giteaToken}` } }); return { ok: true, count: (r.data||[]).length, issues: (r.data||[]).slice(0, 10).map(i => ({ number: i.number, title: i.title, state: i.state })) }; }
|
|
78
|
-
});
|
|
78
|
+
}), 3, "issues-list");
|
|
79
79
|
return asToolResult({ providers: out });
|
|
80
80
|
}
|
|
81
81
|
if (args.action === "comment") {
|
|
82
82
|
if (!args.number) return asToolError("MISSING_PARAMETER", "number é obrigatório para comentar", { parameter: "number" });
|
|
83
83
|
if (!args.body) return asToolError("MISSING_PARAMETER", "body é obrigatório para comentar", { parameter: "body" });
|
|
84
|
-
const out = await runBoth(pm, {
|
|
84
|
+
const out = await withRetry(() => runBoth(pm, {
|
|
85
85
|
github: async (owner) => { await pm.github.rest.issues.createComment({ owner, repo, issue_number: args.number, body: args.body }); return { ok: true }; },
|
|
86
86
|
gitea: async (owner) => { const base = pm.giteaUrl.replace(/\/$/, ""); await axios.post(`${base}/api/v1/repos/${owner}/${repo}/issues/${args.number}/comments`, { body: args.body }, { headers: { Authorization: `token ${pm.giteaToken}` } }); return { ok: true }; }
|
|
87
|
-
});
|
|
87
|
+
}), 3, "issues-comment");
|
|
88
88
|
return asToolResult({ success: !!(out.github?.ok || out.gitea?.ok), issueNumber: args.number, providers: out });
|
|
89
89
|
}
|
|
90
90
|
return asToolError("VALIDATION_ERROR", `Ação '${args.action}' não suportada`, { availableActions: ["create", "list", "comment"] });
|
package/src/tools/git-merge.js
CHANGED
|
@@ -91,6 +91,16 @@ SQUASH:
|
|
|
91
91
|
});
|
|
92
92
|
}
|
|
93
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
|
+
|
|
94
104
|
const currentBranch = await git.getCurrentBranch(projectPath);
|
|
95
105
|
if (currentBranch === args.branch) {
|
|
96
106
|
return asToolError("VALIDATION_ERROR", "Não pode fazer merge de uma branch nela mesma", {
|
|
@@ -99,21 +109,35 @@ SQUASH:
|
|
|
99
109
|
});
|
|
100
110
|
}
|
|
101
111
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
+
}
|
|
117
141
|
}
|
|
118
142
|
|
|
119
143
|
return asToolError("VALIDATION_ERROR", `Ação '${action}' não suportada`, {
|
package/src/tools/git-pulls.js
CHANGED
|
@@ -80,26 +80,26 @@ NOTA: O PR é criado em AMBOS os providers simultaneamente.`;
|
|
|
80
80
|
const base = args.base;
|
|
81
81
|
const title = args.title || `${head} -> ${base}`;
|
|
82
82
|
const body = args.body || "";
|
|
83
|
-
const out = await runBoth(pm, {
|
|
83
|
+
const out = await withRetry(() => runBoth(pm, {
|
|
84
84
|
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 }; },
|
|
85
85
|
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 }; }
|
|
86
|
-
});
|
|
86
|
+
}), 3, "pulls-create");
|
|
87
87
|
return asToolResult({ success: !!(out.github?.ok || out.gitea?.ok), title, head, base, providers: out });
|
|
88
88
|
}
|
|
89
89
|
if (args.action === "list") {
|
|
90
|
-
const out = await runBoth(pm, {
|
|
90
|
+
const out = await withRetry(() => runBoth(pm, {
|
|
91
91
|
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 })) }; },
|
|
92
92
|
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 })) }; }
|
|
93
|
-
});
|
|
93
|
+
}), 3, "pulls-list");
|
|
94
94
|
return asToolResult({ providers: out });
|
|
95
95
|
}
|
|
96
96
|
if (args.action === "files") {
|
|
97
97
|
if (!args.number) return asToolError("MISSING_PARAMETER", "number é obrigatório para ver arquivos do PR", { parameter: "number" });
|
|
98
|
-
const out = await runBoth(pm, {
|
|
98
|
+
const out = await withRetry(() => runBoth(pm, {
|
|
99
99
|
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 })) }; },
|
|
100
100
|
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 })) }; }
|
|
101
|
-
});
|
|
102
|
-
return asToolResult({
|
|
101
|
+
}), 3, "pulls-files");
|
|
102
|
+
return asToolResult({ success: true, providers: out });
|
|
103
103
|
}
|
|
104
104
|
return asToolError("VALIDATION_ERROR", `Ação '${args.action}' não suportada`, { availableActions: ["create", "list", "files"] });
|
|
105
105
|
} catch (e) {
|
package/src/tools/git-remote.js
CHANGED
|
@@ -4,7 +4,7 @@ import fs from "node:fs";
|
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import archiver from "archiver";
|
|
6
6
|
import { asToolError, asToolResult, errorToResponse, mapExternalError } from "../utils/errors.js";
|
|
7
|
-
import { getRepoNameFromPath, validateProjectPath } from "../utils/repoHelpers.js";
|
|
7
|
+
import { getRepoNameFromPath, validateProjectPath, withRetry } from "../utils/repoHelpers.js";
|
|
8
8
|
import { runBoth } from "../utils/providerExec.js";
|
|
9
9
|
|
|
10
10
|
/**
|
package/src/tools/git-reset.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import Ajv from "ajv";
|
|
2
2
|
import { asToolError, asToolResult, errorToResponse } from "../utils/errors.js";
|
|
3
|
-
import { validateProjectPath } from "../utils/repoHelpers.js";
|
|
3
|
+
import { validateProjectPath, withRetry } from "../utils/repoHelpers.js";
|
|
4
4
|
|
|
5
5
|
const ajv = new Ajv({ allErrors: true });
|
|
6
6
|
|
|
@@ -73,19 +73,19 @@ REFERÊNCIAS:
|
|
|
73
73
|
}
|
|
74
74
|
|
|
75
75
|
if (action === "soft") {
|
|
76
|
-
await git.resetSoft(projectPath, ref);
|
|
76
|
+
await withRetry(() => git.resetSoft(projectPath, ref), 3, "reset-soft");
|
|
77
77
|
return asToolResult({ success: true, action: "soft", ref, message: "Commits desfeitos, mudanças mantidas staged" });
|
|
78
78
|
}
|
|
79
79
|
if (action === "mixed") {
|
|
80
|
-
await git.resetMixed(projectPath, ref);
|
|
80
|
+
await withRetry(() => git.resetMixed(projectPath, ref), 3, "reset-mixed");
|
|
81
81
|
return asToolResult({ success: true, action: "mixed", ref, message: "Commits desfeitos, mudanças mantidas no working directory" });
|
|
82
82
|
}
|
|
83
83
|
if (action === "hard") {
|
|
84
|
-
await git.resetHard(projectPath, ref);
|
|
84
|
+
await withRetry(() => git.resetHard(projectPath, ref), 3, "reset-hard");
|
|
85
85
|
return asToolResult({ success: true, action: "hard", ref, message: "⚠️ Reset hard executado - mudanças descartadas" });
|
|
86
86
|
}
|
|
87
87
|
if (action === "hard-clean") {
|
|
88
|
-
const result = await git.resetHardClean(projectPath, ref);
|
|
88
|
+
const result = await withRetry(() => git.resetHardClean(projectPath, ref), 3, "reset-hard-clean");
|
|
89
89
|
return asToolResult({
|
|
90
90
|
success: true,
|
|
91
91
|
action: "hard-clean",
|
package/src/tools/git-stash.js
CHANGED
|
@@ -67,7 +67,7 @@ AÇÕES:
|
|
|
67
67
|
try {
|
|
68
68
|
validateProjectPath(projectPath);
|
|
69
69
|
if (action === "list") {
|
|
70
|
-
const items = await git.listStash(projectPath);
|
|
70
|
+
const items = await withRetry(() => git.listStash(projectPath), 3, "stash-list");
|
|
71
71
|
return asToolResult({
|
|
72
72
|
stashes: items.map((s, i) => ({ index: i, ref: `stash@{${i}}`, message: s.message, timestamp: s.timestamp })),
|
|
73
73
|
count: items.length,
|
|
@@ -79,7 +79,7 @@ AÇÕES:
|
|
|
79
79
|
if (status.isClean && !args.includeUntracked) {
|
|
80
80
|
return asToolError("NOTHING_TO_STASH", "Working tree limpa, nada para stash", { status });
|
|
81
81
|
}
|
|
82
|
-
await git.saveStash(projectPath, args.message || "WIP", !!args.includeUntracked);
|
|
82
|
+
await withRetry(() => git.saveStash(projectPath, args.message || "WIP", !!args.includeUntracked), 3, "stash-save");
|
|
83
83
|
return asToolResult({ success: true, message: args.message || "WIP", savedFiles: status.modified.length + status.created.length + status.not_added.length });
|
|
84
84
|
}
|
|
85
85
|
if (action === "apply") {
|
|
@@ -87,7 +87,7 @@ AÇÕES:
|
|
|
87
87
|
if (items.length === 0) {
|
|
88
88
|
return asToolError("STASH_NOT_FOUND", "Nenhum stash disponível", { availableStashes: [] });
|
|
89
89
|
}
|
|
90
|
-
await git.applyStash(projectPath, args.ref || "stash@{0}");
|
|
90
|
+
await withRetry(() => git.applyStash(projectPath, args.ref || "stash@{0}"), 3, "stash-apply");
|
|
91
91
|
return asToolResult({ success: true, ref: args.ref || "stash@{0}", message: "Stash aplicado (ainda está na lista)" });
|
|
92
92
|
}
|
|
93
93
|
if (action === "pop") {
|
|
@@ -95,7 +95,7 @@ AÇÕES:
|
|
|
95
95
|
if (items.length === 0) {
|
|
96
96
|
return asToolError("STASH_NOT_FOUND", "Nenhum stash disponível", { availableStashes: [] });
|
|
97
97
|
}
|
|
98
|
-
await git.popStash(projectPath, args.ref || "stash@{0}");
|
|
98
|
+
await withRetry(() => git.popStash(projectPath, args.ref || "stash@{0}"), 3, "stash-pop");
|
|
99
99
|
return asToolResult({ success: true, ref: args.ref || "stash@{0}", message: "Stash aplicado e removido da lista" });
|
|
100
100
|
}
|
|
101
101
|
if (action === "drop") {
|
package/src/tools/git-sync.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import Ajv from "ajv";
|
|
2
2
|
import { asToolError, asToolResult, errorToResponse } from "../utils/errors.js";
|
|
3
|
-
import { validateProjectPath } from "../utils/repoHelpers.js";
|
|
3
|
+
import { validateProjectPath, withRetry } from "../utils/repoHelpers.js";
|
|
4
4
|
|
|
5
5
|
const ajv = new Ajv({ allErrors: true });
|
|
6
6
|
|
|
@@ -58,7 +58,11 @@ NOTA: Se pull falhar com conflito, resolva manualmente e faça commit.`;
|
|
|
58
58
|
const branch = args.branch || await git.getCurrentBranch(projectPath);
|
|
59
59
|
if (action === "fetch") {
|
|
60
60
|
const remotes = args.remote ? [args.remote] : ["origin", "github", "gitea"];
|
|
61
|
-
const promises = remotes.map(r =>
|
|
61
|
+
const promises = remotes.map(r => withRetry(
|
|
62
|
+
() => git.fetch(projectPath, r, branch),
|
|
63
|
+
3,
|
|
64
|
+
`fetch-${r}`
|
|
65
|
+
).then(() => ({ remote: r, ok: true })).catch(e => ({ remote: r, ok: false, error: String(e?.message || e) })));
|
|
62
66
|
const results = await Promise.all(promises);
|
|
63
67
|
const successful = results.filter(x => x.ok);
|
|
64
68
|
const failed = results.filter(x => !x.ok);
|
|
@@ -71,17 +75,48 @@ NOTA: Se pull falhar com conflito, resolva manualmente e faça commit.`;
|
|
|
71
75
|
});
|
|
72
76
|
}
|
|
73
77
|
if (action === "pull") {
|
|
78
|
+
// Auto-stash logic
|
|
79
|
+
const status = await git.status(projectPath);
|
|
80
|
+
let stashed = false;
|
|
81
|
+
if (!status.isClean) {
|
|
82
|
+
console.log("[GitSync] Working directory dirty, creating auto-stash...");
|
|
83
|
+
try {
|
|
84
|
+
await git.saveStash(projectPath, `Auto-stash before pull ${new Date().toISOString()}`, true);
|
|
85
|
+
stashed = true;
|
|
86
|
+
} catch (stashError) {
|
|
87
|
+
return asToolError("STASH_FAILED", "Falha ao criar stash automático antes do pull", { error: stashError.message });
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
74
91
|
const remotes = args.remote ? [args.remote] : ["origin", "github", "gitea"];
|
|
75
|
-
const promises = remotes.map(r =>
|
|
92
|
+
const promises = remotes.map(r => withRetry(
|
|
93
|
+
() => git.pull(projectPath, r, branch),
|
|
94
|
+
3,
|
|
95
|
+
`pull-${r}`
|
|
96
|
+
).then(() => ({ remote: r, ok: true })).catch(e => ({ remote: r, ok: false, error: String(e?.message || e) })));
|
|
97
|
+
|
|
76
98
|
const results = await Promise.all(promises);
|
|
77
99
|
const successful = results.filter(x => x.ok);
|
|
78
100
|
const failed = results.filter(x => !x.ok);
|
|
101
|
+
|
|
102
|
+
let stashMessage = "";
|
|
103
|
+
if (stashed) {
|
|
104
|
+
try {
|
|
105
|
+
console.log("[GitSync] Restoring auto-stash...");
|
|
106
|
+
await git.popStash(projectPath);
|
|
107
|
+
stashMessage = " (mudanças locais restauradas)";
|
|
108
|
+
} catch (popError) {
|
|
109
|
+
stashMessage = " (ATENÇÃO: mudanças salvas no stash, mas houve conflito ao restaurar. Use 'git-stash pop' manualmente)";
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
79
113
|
return asToolResult({
|
|
80
114
|
success: successful.length > 0,
|
|
81
115
|
branch,
|
|
82
116
|
pulled: successful.map(x => x.remote),
|
|
83
117
|
failed: failed.map(x => ({ remote: x.remote, error: x.error })),
|
|
84
|
-
message: successful.length === 0 ? "Nenhum remote alcançado.
|
|
118
|
+
message: (successful.length === 0 ? "Nenhum remote alcançado." : `Pull de ${successful.length} remote(s) concluído`) + stashMessage,
|
|
119
|
+
stashed
|
|
85
120
|
});
|
|
86
121
|
}
|
|
87
122
|
return asToolError("VALIDATION_ERROR", `Ação '${action}' não suportada`, { availableActions: ["pull", "fetch"] });
|
package/src/tools/git-tags.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import Ajv from "ajv";
|
|
2
2
|
import { asToolError, asToolResult, errorToResponse } from "../utils/errors.js";
|
|
3
|
-
import { validateProjectPath } from "../utils/repoHelpers.js";
|
|
3
|
+
import { validateProjectPath, withRetry } from "../utils/repoHelpers.js";
|
|
4
4
|
|
|
5
5
|
const ajv = new Ajv({ allErrors: true });
|
|
6
6
|
|
|
@@ -72,7 +72,7 @@ FLUXO TÍPICO:
|
|
|
72
72
|
if (existing.includes(args.tag)) {
|
|
73
73
|
return asToolError("TAG_ALREADY_EXISTS", `Tag '${args.tag}' já existe`, { tag: args.tag, existingTags: existing });
|
|
74
74
|
}
|
|
75
|
-
await git.createTag(projectPath, args.tag, args.ref || "HEAD", args.message);
|
|
75
|
+
await withRetry(() => git.createTag(projectPath, args.tag, args.ref || "HEAD", args.message), 3, "create-tag");
|
|
76
76
|
return asToolResult({ success: true, tag: args.tag, ref: args.ref || "HEAD", message: `Tag '${args.tag}' criada. Use action='push' para enviar ao GitHub/Gitea.` });
|
|
77
77
|
}
|
|
78
78
|
if (action === "delete") {
|
|
@@ -81,7 +81,7 @@ FLUXO TÍPICO:
|
|
|
81
81
|
if (!existing.includes(args.tag)) {
|
|
82
82
|
return asToolError("TAG_NOT_FOUND", `Tag '${args.tag}' não encontrada`, { tag: args.tag, existingTags: existing });
|
|
83
83
|
}
|
|
84
|
-
await git.deleteTag(projectPath, args.tag);
|
|
84
|
+
await withRetry(() => git.deleteTag(projectPath, args.tag), 3, "delete-tag");
|
|
85
85
|
return asToolResult({ success: true, tag: args.tag, deleted: true });
|
|
86
86
|
}
|
|
87
87
|
if (action === "push") {
|
|
@@ -91,8 +91,8 @@ FLUXO TÍPICO:
|
|
|
91
91
|
return asToolError("TAG_NOT_FOUND", `Tag '${args.tag}' não existe localmente. Crie primeiro com action='create'`, { tag: args.tag, existingTags: existing });
|
|
92
92
|
}
|
|
93
93
|
const results = await Promise.allSettled([
|
|
94
|
-
git.pushTag(projectPath, "github", args.tag),
|
|
95
|
-
git.pushTag(projectPath, "gitea", args.tag)
|
|
94
|
+
withRetry(() => git.pushTag(projectPath, "github", args.tag), 3, "push-tag-github"),
|
|
95
|
+
withRetry(() => git.pushTag(projectPath, "gitea", args.tag), 3, "push-tag-gitea")
|
|
96
96
|
]);
|
|
97
97
|
const pushed = [];
|
|
98
98
|
const failed = [];
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import Ajv from "ajv";
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import path from "path";
|
|
2
4
|
import { MCPError, asToolError, asToolResult, errorToResponse, createError } from "../utils/errors.js";
|
|
3
|
-
import { getRepoNameFromPath, detectProjectType, GITIGNORE_TEMPLATES, validateProjectPath } from "../utils/repoHelpers.js";
|
|
5
|
+
import { getRepoNameFromPath, detectProjectType, GITIGNORE_TEMPLATES, validateProjectPath, withRetry } from "../utils/repoHelpers.js";
|
|
4
6
|
|
|
5
7
|
const ajv = new Ajv({ allErrors: true });
|
|
6
8
|
|
|
@@ -282,7 +284,13 @@ EXEMPLOS DE USO:
|
|
|
282
284
|
});
|
|
283
285
|
}
|
|
284
286
|
|
|
285
|
-
|
|
287
|
+
// Retry logic for push (often fails due to network or concurrent updates)
|
|
288
|
+
const result = await withRetry(
|
|
289
|
+
() => git.pushParallel(projectPath, branch, force),
|
|
290
|
+
3,
|
|
291
|
+
"push"
|
|
292
|
+
);
|
|
293
|
+
|
|
286
294
|
return asToolResult({ success: true, branch, ...result });
|
|
287
295
|
}
|
|
288
296
|
return asToolError("VALIDATION_ERROR", `Ação '${action}' não suportada`, {
|
package/src/utils/gitAdapter.js
CHANGED
|
@@ -2,7 +2,7 @@ import { spawn } from "node:child_process";
|
|
|
2
2
|
import fs from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { MCPError, createError, mapExternalError } from "./errors.js";
|
|
5
|
-
import { getRepoNameFromPath, getProvidersEnv } from "./repoHelpers.js";
|
|
5
|
+
import { getRepoNameFromPath, getProvidersEnv, findGitRoot } from "./repoHelpers.js";
|
|
6
6
|
|
|
7
7
|
// Common locations for Git on Windows, Linux, and macOS
|
|
8
8
|
const GIT_CANDIDATES = [
|
package/src/utils/repoHelpers.js
CHANGED
|
@@ -66,6 +66,26 @@ export function validateFilePath(projectPath, filePath) {
|
|
|
66
66
|
return absoluteFile;
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
+
/**
|
|
70
|
+
* Encontra a raiz do repositório Git subindo a árvore de diretórios
|
|
71
|
+
* @param {string} startDir - Diretório inicial
|
|
72
|
+
* @returns {string|null} Caminho da raiz do repo ou null se não encontrado
|
|
73
|
+
*/
|
|
74
|
+
export function findGitRoot(startDir) {
|
|
75
|
+
let current = path.resolve(startDir);
|
|
76
|
+
const root = path.parse(current).root;
|
|
77
|
+
|
|
78
|
+
while (true) {
|
|
79
|
+
if (fs.existsSync(path.join(current, ".git"))) {
|
|
80
|
+
return current;
|
|
81
|
+
}
|
|
82
|
+
if (current === root) {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
current = path.dirname(current);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
69
89
|
export function getEnv(key) {
|
|
70
90
|
const v = process.env[key];
|
|
71
91
|
return v === undefined ? "" : String(v);
|
|
@@ -158,3 +178,39 @@ export function detectProjectType(projectPath) {
|
|
|
158
178
|
return "general";
|
|
159
179
|
}
|
|
160
180
|
|
|
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
|
|
187
|
+
*/
|
|
188
|
+
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;
|
|
215
|
+
}
|
|
216
|
+
|