@andre.buzeli/git-mcp 16.1.3 → 16.1.6
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 +3 -2
- package/package.json +14 -3
- package/src/index.js +38 -42
- package/src/resources/index.js +79 -41
- package/src/tools/git-branches.js +5 -2
- package/src/tools/git-clone.js +13 -2
- package/src/tools/git-config.js +5 -2
- package/src/tools/git-diff.js +27 -6
- package/src/tools/git-files.js +5 -2
- package/src/tools/git-help.js +8 -3
- package/src/tools/git-history.js +9 -4
- package/src/tools/git-ignore.js +5 -2
- package/src/tools/git-issues.js +5 -2
- package/src/tools/git-merge.js +12 -7
- package/src/tools/git-pulls.js +5 -2
- package/src/tools/git-remote.js +10 -3
- package/src/tools/git-reset.js +5 -2
- package/src/tools/git-stash.js +5 -2
- package/src/tools/git-sync.js +5 -2
- package/src/tools/git-tags.js +5 -2
- package/src/tools/git-workflow.js +97 -42
- package/src/tools/git-worktree.js +66 -38
- package/src/utils/errors.js +21 -0
- package/src/utils/gitAdapter.js +165 -125
- package/src/utils/retry.js +1 -1
- package/src/utils/worktreeResolver.js +189 -0
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
|
|
4
|
+
const PROTECTED_PATH_PREFIXES = [
|
|
5
|
+
'C:\\Users',
|
|
6
|
+
'C:/Users'
|
|
7
|
+
];
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Verifica se um caminho é protegido e não deve receber git init/worktree.
|
|
11
|
+
* Normaliza separadores e compara case-insensitive.
|
|
12
|
+
* @param {string} p - Caminho absoluto a verificar
|
|
13
|
+
* @returns {boolean} - True se for protegido
|
|
14
|
+
*/
|
|
15
|
+
export function isProtectedPath(p) {
|
|
16
|
+
if (!p) return false;
|
|
17
|
+
|
|
18
|
+
const normalized = p.replace(/\\/g, '/').toLowerCase();
|
|
19
|
+
|
|
20
|
+
// Verifica se o caminho é temporário (permitido) APENAS em ambiente de teste
|
|
21
|
+
// Isso é importante para testes em ambientes onde o temp está dentro de Users
|
|
22
|
+
if (process.env.NODE_ENV === 'test') {
|
|
23
|
+
const tempDir = path.resolve(process.env.TEMP || process.env.TMP || '/tmp').replace(/\\/g, '/').toLowerCase();
|
|
24
|
+
if (normalized.startsWith(tempDir)) {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
for (const prefix of PROTECTED_PATH_PREFIXES) {
|
|
30
|
+
const normalizedPrefix = prefix.replace(/\\/g, '/').toLowerCase();
|
|
31
|
+
|
|
32
|
+
// Verifica igualdade exata ou se é subdiretório
|
|
33
|
+
if (normalized === normalizedPrefix || normalized.startsWith(normalizedPrefix + '/')) {
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Busca ascendente por um diretório ou arquivo .git com limite de profundidade.
|
|
43
|
+
* @param {string} startPath - Caminho inicial absoluto
|
|
44
|
+
* @param {number} maxDepth - Número máximo de níveis para subir (default: 2)
|
|
45
|
+
* @returns {string|null} - Caminho do diretório contendo .git ou null se não encontrar
|
|
46
|
+
*/
|
|
47
|
+
export function findGitRoot(startPath, maxDepth = 2) {
|
|
48
|
+
let current = path.resolve(startPath);
|
|
49
|
+
let depth = 0;
|
|
50
|
+
|
|
51
|
+
// Verifica o próprio startPath primeiro (depth 0)
|
|
52
|
+
if (fs.existsSync(path.join(current, '.git'))) {
|
|
53
|
+
return current;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Sobe na árvore até maxDepth níveis ACIMA do startPath
|
|
57
|
+
while (depth < maxDepth) {
|
|
58
|
+
const parent = path.dirname(current);
|
|
59
|
+
if (parent === current) {
|
|
60
|
+
return null; // Chegou na raiz do FS
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
current = parent;
|
|
64
|
+
depth++;
|
|
65
|
+
|
|
66
|
+
if (fs.existsSync(path.join(current, '.git'))) {
|
|
67
|
+
return current;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Resolve o contexto de worktree para um dado caminho de projeto.
|
|
76
|
+
* Detecta se é worktree, repo principal, ou se precisa de auto-init.
|
|
77
|
+
* @param {string} projectPath - Caminho do projeto fornecido pelo agente
|
|
78
|
+
* @param {GitAdapter} gitAdapter - Instância do GitAdapter para executar comandos
|
|
79
|
+
* @returns {Promise<Object>} - WorktreeContext resolvido
|
|
80
|
+
*/
|
|
81
|
+
export async function resolveWorktreeContext(projectPath, gitAdapter) {
|
|
82
|
+
const absolutePath = path.resolve(projectPath);
|
|
83
|
+
|
|
84
|
+
// Tenta encontrar .git no path ou acima (max 2 níveis)
|
|
85
|
+
// Se encontrar .git no próprio projectPath (depth 0), usa ele.
|
|
86
|
+
// Se encontrar acima, usa o pai como base.
|
|
87
|
+
let root = findGitRoot(absolutePath, 2);
|
|
88
|
+
|
|
89
|
+
if (!root) {
|
|
90
|
+
// Caso não encontre .git em até 2 níveis: Auto-init
|
|
91
|
+
if (isProtectedPath(absolutePath)) {
|
|
92
|
+
throw new Error(`PROTECTED_PATH: não é permitido criar repositório git em ${absolutePath}`);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Executa git init
|
|
96
|
+
await gitAdapter._exec(absolutePath, ['init']);
|
|
97
|
+
|
|
98
|
+
// Após init, obtém a branch (provavelmente master/main)
|
|
99
|
+
const branch = await getBranchName(absolutePath, gitAdapter);
|
|
100
|
+
|
|
101
|
+
return {
|
|
102
|
+
repoRoot: absolutePath,
|
|
103
|
+
worktreePath: absolutePath,
|
|
104
|
+
branch: branch,
|
|
105
|
+
isWorktree: false,
|
|
106
|
+
gitCommonDir: path.join(absolutePath, '.git'),
|
|
107
|
+
autoInitialized: true
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Se encontrou raiz (pode ser o próprio projectPath ou um pai)
|
|
112
|
+
const gitEntry = path.join(root, '.git');
|
|
113
|
+
const stats = fs.statSync(gitEntry);
|
|
114
|
+
|
|
115
|
+
if (stats.isDirectory()) {
|
|
116
|
+
const branch = await getBranchName(root, gitAdapter);
|
|
117
|
+
|
|
118
|
+
// Req 9.2: valida que gitCommonDir contém objects/
|
|
119
|
+
if (!fs.existsSync(path.join(gitEntry, 'objects'))) {
|
|
120
|
+
throw new Error(`WORKTREE_CORRUPT: Diretório objects não encontrado em ${gitEntry}`);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return {
|
|
124
|
+
repoRoot: root,
|
|
125
|
+
worktreePath: root,
|
|
126
|
+
branch: branch,
|
|
127
|
+
isWorktree: false,
|
|
128
|
+
gitCommonDir: gitEntry
|
|
129
|
+
};
|
|
130
|
+
} else if (stats.isFile()) {
|
|
131
|
+
// Worktree
|
|
132
|
+
const content = fs.readFileSync(gitEntry, 'utf8');
|
|
133
|
+
const match = content.match(/^gitdir:\s*(.+)$/m);
|
|
134
|
+
|
|
135
|
+
if (!match) {
|
|
136
|
+
throw new Error('WORKTREE_CORRUPT: Arquivo .git inválido ou sem formato gitdir');
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const gitdirRelative = match[1].trim();
|
|
140
|
+
const gitdirAbsolute = path.resolve(root, gitdirRelative);
|
|
141
|
+
|
|
142
|
+
if (!fs.existsSync(gitdirAbsolute)) {
|
|
143
|
+
throw new Error(`WORKTREE_STALE: gitdir ${gitdirAbsolute} não encontrado. Execute 'git worktree prune' no repo principal.`);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Tenta achar commondir
|
|
147
|
+
const commondirFile = path.join(gitdirAbsolute, 'commondir');
|
|
148
|
+
let gitCommonDir;
|
|
149
|
+
|
|
150
|
+
if (fs.existsSync(commondirFile)) {
|
|
151
|
+
const commondirRelative = fs.readFileSync(commondirFile, 'utf8').trim();
|
|
152
|
+
gitCommonDir = path.resolve(gitdirAbsolute, commondirRelative);
|
|
153
|
+
} else {
|
|
154
|
+
gitCommonDir = gitdirAbsolute;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Valida objects
|
|
158
|
+
if (!fs.existsSync(path.join(gitCommonDir, 'objects'))) {
|
|
159
|
+
throw new Error(`WORKTREE_CORRUPT: Diretório objects não encontrado em ${gitCommonDir}`);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const repoRoot = path.dirname(gitCommonDir);
|
|
163
|
+
const branch = await getBranchName(root, gitAdapter);
|
|
164
|
+
|
|
165
|
+
return {
|
|
166
|
+
repoRoot: repoRoot,
|
|
167
|
+
worktreePath: root,
|
|
168
|
+
branch: branch,
|
|
169
|
+
isWorktree: true,
|
|
170
|
+
gitCommonDir: gitCommonDir
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
throw new Error('NOT_A_GIT_REPO: .git entry is neither file nor directory');
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Helper para obter o nome da branch atual
|
|
179
|
+
*/
|
|
180
|
+
async function getBranchName(cwd, gitAdapter) {
|
|
181
|
+
try {
|
|
182
|
+
const result = await gitAdapter._exec(cwd, ['rev-parse', '--abbrev-ref', 'HEAD']);
|
|
183
|
+
return result.trim();
|
|
184
|
+
} catch (error) {
|
|
185
|
+
// Em repo recém inicializado sem commits, pode falhar ou retornar HEAD.
|
|
186
|
+
// Mas git init cria branch default.
|
|
187
|
+
return 'HEAD';
|
|
188
|
+
}
|
|
189
|
+
}
|