@andre.buzeli/git-mcp 16.1.2 → 16.1.4
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 +14 -3
- package/src/index.js +58 -7
- package/src/resources/index.js +82 -44
- package/src/tools/git-clone.js +8 -0
- package/src/tools/git-diff.js +23 -4
- package/src/tools/git-help.js +8 -3
- package/src/tools/git-history.js +6 -2
- package/src/tools/git-merge.js +7 -5
- package/src/tools/git-remote.js +5 -1
- package/src/tools/git-stash.js +1 -0
- package/src/tools/git-workflow.js +98 -41
- package/src/tools/git-worktree.js +61 -36
- package/src/utils/gitAdapter.js +165 -125
- package/src/utils/retry.js +1 -1
- package/src/utils/worktreeResolver.js +189 -0
package/src/utils/gitAdapter.js
CHANGED
|
@@ -315,10 +315,28 @@ export class GitAdapter {
|
|
|
315
315
|
suggestion: "Use git-workflow init para criar um repositório"
|
|
316
316
|
});
|
|
317
317
|
}
|
|
318
|
+
// Worktrees têm .git como arquivo, repos normais como diretório.
|
|
319
|
+
// Ambos são válidos e fs.existsSync retorna true para ambos.
|
|
318
320
|
}
|
|
319
321
|
|
|
320
322
|
/**
|
|
321
|
-
* Verifica se o diretório é um
|
|
323
|
+
* Verifica se o diretório é um worktree (possui arquivo .git)
|
|
324
|
+
* @param {string} dir - Diretório para verificar
|
|
325
|
+
* @returns {boolean} - true se é um worktree
|
|
326
|
+
*/
|
|
327
|
+
async isWorktree(dir) {
|
|
328
|
+
const gitPath = path.join(dir, ".git");
|
|
329
|
+
if (!fs.existsSync(gitPath)) return false;
|
|
330
|
+
try {
|
|
331
|
+
const stats = fs.statSync(gitPath);
|
|
332
|
+
return stats.isFile();
|
|
333
|
+
} catch {
|
|
334
|
+
return false;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Verifica se o diretório é um repositório git (repo principal ou worktree)
|
|
322
340
|
* @param {string} dir - Diretório para verificar
|
|
323
341
|
* @returns {boolean} - true se é um repo git
|
|
324
342
|
*/
|
|
@@ -842,8 +860,12 @@ export class GitAdapter {
|
|
|
842
860
|
try { await this._exec(dir, args); } catch { }
|
|
843
861
|
}
|
|
844
862
|
|
|
845
|
-
async listConfig(dir) {
|
|
846
|
-
const
|
|
863
|
+
async listConfig(dir, scope) {
|
|
864
|
+
const args = ["config", "--list"];
|
|
865
|
+
if (scope === "global") args.push("--global");
|
|
866
|
+
else if (scope === "system") args.push("--system");
|
|
867
|
+
else if (scope === "local") args.push("--local");
|
|
868
|
+
const out = await this._exec(dir, args);
|
|
847
869
|
const items = {};
|
|
848
870
|
out.split("\n").filter(Boolean).forEach(line => {
|
|
849
871
|
const [k, ...v] = line.split("=");
|
|
@@ -959,16 +981,26 @@ export class GitAdapter {
|
|
|
959
981
|
}
|
|
960
982
|
|
|
961
983
|
// ============ DIFF/CLONE ============
|
|
962
|
-
async diff(dir, options = {}) {
|
|
984
|
+
async diff(dir, options = {}) {
|
|
985
|
+
const args = ["diff"];
|
|
986
|
+
if (options.staged) {
|
|
987
|
+
args.push("--cached");
|
|
988
|
+
} else if (options.target) {
|
|
989
|
+
if (options.source) args.push(options.source);
|
|
990
|
+
args.push(options.target);
|
|
991
|
+
}
|
|
992
|
+
return await this._exec(dir, args);
|
|
993
|
+
}
|
|
963
994
|
async diffCommits(dir, from, to) { return await this._exec(dir, ["diff", from, to]); }
|
|
964
995
|
|
|
965
|
-
async clone(
|
|
966
|
-
const { branch, depth, singleBranch } = options;
|
|
996
|
+
async clone(dir, url, options = {}) {
|
|
997
|
+
const { name, branch, depth, singleBranch } = options;
|
|
967
998
|
const args = ["clone"];
|
|
968
999
|
if (branch) args.push("-b", branch);
|
|
969
1000
|
if (depth) args.push("--depth", depth.toString());
|
|
970
1001
|
if (singleBranch) args.push("--single-branch");
|
|
971
|
-
args.push(url
|
|
1002
|
+
args.push(url);
|
|
1003
|
+
if (name) args.push(name);
|
|
972
1004
|
|
|
973
1005
|
const header = this._getAuthHeader(url);
|
|
974
1006
|
const cmdArgs = [];
|
|
@@ -998,9 +1030,10 @@ export class GitAdapter {
|
|
|
998
1030
|
async removeFromGitignore(dir, patterns) {
|
|
999
1031
|
const p = path.join(dir, ".gitignore");
|
|
1000
1032
|
if (!fs.existsSync(p)) return;
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1033
|
+
const lines = fs.readFileSync(p, "utf8").split("\n");
|
|
1034
|
+
const patternSet = new Set(patterns);
|
|
1035
|
+
const filtered = lines.filter(line => !patternSet.has(line.trim()));
|
|
1036
|
+
fs.writeFileSync(p, filtered.join("\n"));
|
|
1004
1037
|
}
|
|
1005
1038
|
|
|
1006
1039
|
async listRemotesRaw(dir) {
|
|
@@ -1029,121 +1062,128 @@ export class GitAdapter {
|
|
|
1029
1062
|
return Array.from(map.values());
|
|
1030
1063
|
}
|
|
1031
1064
|
|
|
1032
|
-
// ============ WORKTREE ============
|
|
1033
|
-
|
|
1034
|
-
async addWorktree(dir, branch, worktreePath) {
|
|
1035
|
-
|
|
1036
|
-
const
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
if (
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
const
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
current.
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
const
|
|
1092
|
-
const
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
await this._exec(dir,
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1065
|
+
// ============ WORKTREE ============
|
|
1066
|
+
|
|
1067
|
+
async addWorktree(dir, branch, worktreePath, force = false) {
|
|
1068
|
+
const branches = await this.listBranches(dir);
|
|
1069
|
+
const branchExists = branches.includes(branch);
|
|
1070
|
+
|
|
1071
|
+
const args = ["worktree", "add"];
|
|
1072
|
+
if (force) args.push("--force");
|
|
1073
|
+
|
|
1074
|
+
if (branchExists) {
|
|
1075
|
+
// Branch já existe: checkout dela no novo worktree
|
|
1076
|
+
args.push(worktreePath, branch);
|
|
1077
|
+
} else {
|
|
1078
|
+
// Branch nova: cria a partir do HEAD atual
|
|
1079
|
+
args.push("-b", branch, worktreePath);
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
await this._exec(dir, args);
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
async listWorktrees(dir) {
|
|
1086
|
+
try {
|
|
1087
|
+
const out = await this._exec(dir, ["worktree", "list", "--porcelain"]);
|
|
1088
|
+
const worktrees = [];
|
|
1089
|
+
let current = {};
|
|
1090
|
+
|
|
1091
|
+
const lines = out.split("\n");
|
|
1092
|
+
for (const line of lines) {
|
|
1093
|
+
if (!line.trim()) {
|
|
1094
|
+
if (current.worktree) {
|
|
1095
|
+
worktrees.push(current);
|
|
1096
|
+
current = {};
|
|
1097
|
+
}
|
|
1098
|
+
continue;
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
const [key, ...rest] = line.split(" ");
|
|
1102
|
+
const value = rest.join(" ");
|
|
1103
|
+
|
|
1104
|
+
if (key === "worktree") {
|
|
1105
|
+
if (current.worktree) worktrees.push(current);
|
|
1106
|
+
current = { worktree: value };
|
|
1107
|
+
} else if (key === "branch") {
|
|
1108
|
+
current.branch = value.replace("refs/heads/", "");
|
|
1109
|
+
} else if (key === "HEAD") {
|
|
1110
|
+
current.head = value;
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
if (current.worktree) worktrees.push(current);
|
|
1114
|
+
|
|
1115
|
+
return worktrees.map(w => ({
|
|
1116
|
+
path: w.worktree,
|
|
1117
|
+
branch: w.branch,
|
|
1118
|
+
head: w.head
|
|
1119
|
+
}));
|
|
1120
|
+
} catch (e) {
|
|
1121
|
+
// Fallback para versão antiga do git se --porcelain falhar
|
|
1122
|
+
const out = await this._exec(dir, ["worktree", "list"]);
|
|
1123
|
+
return out.split("\n").filter(Boolean).map(line => {
|
|
1124
|
+
const parts = line.split(/\s+/);
|
|
1125
|
+
const path = parts.slice(0, -2).join(" "); // Path pode ter espaços
|
|
1126
|
+
const head = parts[parts.length - 2];
|
|
1127
|
+
const branchRaw = parts[parts.length - 1];
|
|
1128
|
+
const branch = branchRaw.replace("[", "").replace("]", "");
|
|
1129
|
+
return { path, head, branch };
|
|
1130
|
+
});
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
async removeWorktree(dir, worktreePath) {
|
|
1135
|
+
await this._exec(dir, ["worktree", "remove", worktreePath, "--force"]);
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
async pruneWorktrees(dir) {
|
|
1139
|
+
await this._exec(dir, ["worktree", "prune"]);
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
async pushRefspec(dir, remote, localBranch, remoteBranch, force = false) {
|
|
1143
|
+
const remoteUrl = await this._exec(dir, ["remote", "get-url", remote]);
|
|
1144
|
+
const header = this._getAuthHeader(remoteUrl);
|
|
1145
|
+
const args = [];
|
|
1146
|
+
if (header) args.push("-c", `http.extraHeader=${header}`);
|
|
1147
|
+
args.push("push");
|
|
1148
|
+
if (force) args.push("--force");
|
|
1149
|
+
args.push(remote, `${localBranch}:${remoteBranch}`);
|
|
1150
|
+
await this._exec(dir, args);
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
async getWorktreeConfigs(dir) {
|
|
1154
|
+
try {
|
|
1155
|
+
const configPath = path.join(dir, ".git", "config");
|
|
1156
|
+
if (!fs.existsSync(configPath)) return [];
|
|
1157
|
+
|
|
1158
|
+
const content = fs.readFileSync(configPath, "utf8");
|
|
1159
|
+
const results = [];
|
|
1160
|
+
const sectionRegex = /\[worktree-branch "([^"]+)"\]([\s\S]*?)(?=\[|$)/g;
|
|
1161
|
+
|
|
1162
|
+
let match;
|
|
1163
|
+
while ((match = sectionRegex.exec(content)) !== null) {
|
|
1164
|
+
const branchName = match[1];
|
|
1165
|
+
const body = match[2];
|
|
1166
|
+
const wtPath = body.match(/path\s*=\s*(.+)/)?.[1]?.trim();
|
|
1167
|
+
const channel = body.match(/channel\s*=\s*(.+)/)?.[1]?.trim() || "production";
|
|
1168
|
+
if (branchName && wtPath) {
|
|
1169
|
+
results.push({ branch: branchName, path: wtPath, channel });
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
return results;
|
|
1173
|
+
} catch (e) {
|
|
1174
|
+
console.error("[GitAdapter] Error reading worktree configs:", e);
|
|
1175
|
+
return [];
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
async setWorktreeConfig(dir, branchName, config) {
|
|
1180
|
+
if (config.path !== undefined) {
|
|
1181
|
+
const normalizedPath = config.path.replace(/\\/g, "/");
|
|
1182
|
+
await this._exec(dir, ["config", `worktree-branch.${branchName}.path`, normalizedPath]);
|
|
1183
|
+
}
|
|
1184
|
+
await this._exec(dir, ["config", `worktree-branch.${branchName}.channel`, config.channel || "production"]);
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1147
1187
|
// ============ GIT LFS SUPPORT ============
|
|
1148
1188
|
|
|
1149
1189
|
/**
|
package/src/utils/retry.js
CHANGED
|
@@ -19,7 +19,7 @@ const DEFAULT_OPTIONS = {
|
|
|
19
19
|
|
|
20
20
|
function shouldRetry(error, options) {
|
|
21
21
|
const msg = (error?.message || String(error)).toLowerCase();
|
|
22
|
-
const code = error?.code
|
|
22
|
+
const code = String(error?.code ?? "").toLowerCase();
|
|
23
23
|
|
|
24
24
|
return options.retryableErrors.some(e =>
|
|
25
25
|
msg.includes(e.toLowerCase()) || code.includes(e.toLowerCase())
|
|
@@ -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
|
+
}
|