@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.
@@ -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
+ }