@andrebuzeli/git-mcp 7.6.1 → 8.0.1
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 +309 -0
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/providers/giteaProvider.d.ts +17 -1
- package/dist/providers/giteaProvider.d.ts.map +1 -1
- package/dist/providers/giteaProvider.js +86 -1
- package/dist/providers/giteaProvider.js.map +1 -1
- package/dist/providers/providerManager.d.ts +2 -0
- package/dist/providers/providerManager.d.ts.map +1 -1
- package/dist/providers/providerManager.js +3 -0
- package/dist/providers/providerManager.js.map +1 -1
- package/dist/server.d.ts +2 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +1 -1
- package/dist/server.js.map +1 -1
- package/dist/tools/gitAnalytics.d.ts.map +1 -1
- package/dist/tools/gitAnalytics.js +5 -0
- package/dist/tools/gitAnalytics.js.map +1 -1
- package/dist/tools/gitBackup.d.ts +2 -2
- package/dist/tools/gitBackup.d.ts.map +1 -1
- package/dist/tools/gitBackup.js +5 -6
- package/dist/tools/gitBackup.js.map +1 -1
- package/dist/tools/gitBranches.d.ts.map +1 -1
- package/dist/tools/gitBranches.js +35 -21
- package/dist/tools/gitBranches.js.map +1 -1
- package/dist/tools/gitConfig.d.ts +2 -2
- package/dist/tools/gitConfig.d.ts.map +1 -1
- package/dist/tools/gitConfig.js +7 -8
- package/dist/tools/gitConfig.js.map +1 -1
- package/dist/tools/gitFix.d.ts +2 -1
- package/dist/tools/gitFix.d.ts.map +1 -1
- package/dist/tools/gitFix.js +24 -17
- package/dist/tools/gitFix.js.map +1 -1
- package/dist/tools/gitFix.tool.d.ts +2 -2
- package/dist/tools/gitFix.tool.d.ts.map +1 -1
- package/dist/tools/gitFix.tool.js +2 -2
- package/dist/tools/gitFix.tool.js.map +1 -1
- package/dist/tools/gitHistory.d.ts.map +1 -1
- package/dist/tools/gitHistory.js +9 -10
- package/dist/tools/gitHistory.js.map +1 -1
- package/dist/tools/gitIssues.d.ts.map +1 -1
- package/dist/tools/gitIssues.js +43 -12
- package/dist/tools/gitIssues.js.map +1 -1
- package/dist/tools/gitMonitor.d.ts.map +1 -1
- package/dist/tools/gitMonitor.js +9 -10
- package/dist/tools/gitMonitor.js.map +1 -1
- package/dist/tools/gitPulls.d.ts.map +1 -1
- package/dist/tools/gitPulls.js +23 -4
- package/dist/tools/gitPulls.js.map +1 -1
- package/dist/tools/gitRelease.d.ts.map +1 -1
- package/dist/tools/gitRelease.js +34 -3
- package/dist/tools/gitRelease.js.map +1 -1
- package/dist/tools/gitRemote.d.ts +6 -1
- package/dist/tools/gitRemote.d.ts.map +1 -1
- package/dist/tools/gitRemote.js +17 -9
- package/dist/tools/gitRemote.js.map +1 -1
- package/dist/tools/gitReset.d.ts.map +1 -1
- package/dist/tools/gitReset.js +6 -7
- package/dist/tools/gitReset.js.map +1 -1
- package/dist/tools/gitStash.d.ts +2 -2
- package/dist/tools/gitStash.d.ts.map +1 -1
- package/dist/tools/gitStash.js +21 -25
- package/dist/tools/gitStash.js.map +1 -1
- package/dist/tools/gitSync.d.ts.map +1 -1
- package/dist/tools/gitSync.js +16 -19
- package/dist/tools/gitSync.js.map +1 -1
- package/dist/tools/gitTags.d.ts.map +1 -1
- package/dist/tools/gitTags.js +12 -23
- package/dist/tools/gitTags.js.map +1 -1
- package/dist/tools/gitUpdate.d.ts.map +1 -1
- package/dist/tools/gitUpdate.js +28 -41
- package/dist/tools/gitUpdate.js.map +1 -1
- package/dist/tools/gitUpload.d.ts.map +1 -1
- package/dist/tools/gitUpload.js +19 -21
- package/dist/tools/gitUpload.js.map +1 -1
- package/dist/tools/gitWorkflow.d.ts.map +1 -1
- package/dist/tools/gitWorkflow.js +21 -39
- package/dist/tools/gitWorkflow.js.map +1 -1
- package/dist/types.d.ts +2 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/apiHelpers.d.ts +30 -0
- package/dist/utils/apiHelpers.d.ts.map +1 -0
- package/dist/utils/apiHelpers.js +126 -0
- package/dist/utils/apiHelpers.js.map +1 -0
- package/dist/utils/gitAdapter.d.ts +222 -0
- package/dist/utils/gitAdapter.d.ts.map +1 -0
- package/dist/utils/gitAdapter.js +1071 -0
- package/dist/utils/gitAdapter.js.map +1 -0
- package/package.json +12 -4
|
@@ -0,0 +1,1071 @@
|
|
|
1
|
+
import * as git from 'isomorphic-git';
|
|
2
|
+
import * as fs from 'fs';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
import http from 'isomorphic-git/http/node';
|
|
5
|
+
// ============================================================================
|
|
6
|
+
// ISOMORPHIC GIT ADAPTER IMPLEMENTATION
|
|
7
|
+
// ============================================================================
|
|
8
|
+
export class IsomorphicGitAdapter {
|
|
9
|
+
constructor(providerManager) {
|
|
10
|
+
this.providerManager = providerManager;
|
|
11
|
+
}
|
|
12
|
+
// ========================================================================
|
|
13
|
+
// HELPER METHODS
|
|
14
|
+
// ========================================================================
|
|
15
|
+
/**
|
|
16
|
+
* Get authentication callback for HTTP operations
|
|
17
|
+
*/
|
|
18
|
+
getAuthCallback(remote) {
|
|
19
|
+
return () => {
|
|
20
|
+
// Determine if this is GitHub or Gitea based on remote URL
|
|
21
|
+
const isGitHub = remote.includes('github.com');
|
|
22
|
+
if (isGitHub) {
|
|
23
|
+
const token = process.env.GITHUB_TOKEN;
|
|
24
|
+
if (!token) {
|
|
25
|
+
throw new Error('GITHUB_TOKEN not found in environment variables');
|
|
26
|
+
}
|
|
27
|
+
return {
|
|
28
|
+
username: token,
|
|
29
|
+
password: 'x-oauth-basic',
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
const token = process.env.GITEA_TOKEN;
|
|
34
|
+
if (!token) {
|
|
35
|
+
throw new Error('GITEA_TOKEN not found in environment variables');
|
|
36
|
+
}
|
|
37
|
+
return {
|
|
38
|
+
username: token,
|
|
39
|
+
password: 'x-oauth-basic',
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Get author information from provider or fallback to config/default
|
|
46
|
+
*/
|
|
47
|
+
async getAuthor(dir, providedAuthor) {
|
|
48
|
+
if (providedAuthor) {
|
|
49
|
+
return {
|
|
50
|
+
...providedAuthor,
|
|
51
|
+
timestamp: providedAuthor.timestamp || Math.floor(Date.now() / 1000),
|
|
52
|
+
timezoneOffset: providedAuthor.timezoneOffset || new Date().getTimezoneOffset(),
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
// Try to get from provider manager (GitHub/Gitea username)
|
|
56
|
+
try {
|
|
57
|
+
const githubToken = process.env.GITHUB_TOKEN;
|
|
58
|
+
const giteaToken = process.env.GITEA_TOKEN;
|
|
59
|
+
if (githubToken && this.providerManager.github) {
|
|
60
|
+
const user = await this.providerManager.github.rest.users.getAuthenticated();
|
|
61
|
+
return {
|
|
62
|
+
name: user.data.name || user.data.login,
|
|
63
|
+
email: user.data.email || `${user.data.login}@users.noreply.github.com`,
|
|
64
|
+
timestamp: Math.floor(Date.now() / 1000),
|
|
65
|
+
timezoneOffset: new Date().getTimezoneOffset(),
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
if (giteaToken && this.providerManager.giteaBaseUrl) {
|
|
69
|
+
const axios = (await import('axios')).default;
|
|
70
|
+
const response = await axios.get(`${this.providerManager.giteaBaseUrl}/api/v1/user`, {
|
|
71
|
+
headers: { Authorization: `token ${giteaToken}` },
|
|
72
|
+
});
|
|
73
|
+
const user = response.data;
|
|
74
|
+
return {
|
|
75
|
+
name: user.full_name || user.login,
|
|
76
|
+
email: user.email || `${user.login}@gitea.local`,
|
|
77
|
+
timestamp: Math.floor(Date.now() / 1000),
|
|
78
|
+
timezoneOffset: new Date().getTimezoneOffset(),
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
catch (err) {
|
|
83
|
+
// Continue to fallback
|
|
84
|
+
}
|
|
85
|
+
// Try to get from local Git config
|
|
86
|
+
try {
|
|
87
|
+
const name = await git.getConfig({ fs, dir, path: 'user.name' });
|
|
88
|
+
const email = await git.getConfig({ fs, dir, path: 'user.email' });
|
|
89
|
+
if (name && email) {
|
|
90
|
+
return {
|
|
91
|
+
name,
|
|
92
|
+
email,
|
|
93
|
+
timestamp: Math.floor(Date.now() / 1000),
|
|
94
|
+
timezoneOffset: new Date().getTimezoneOffset(),
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
catch (err) {
|
|
99
|
+
// Continue to fallback
|
|
100
|
+
}
|
|
101
|
+
// Default fallback
|
|
102
|
+
return {
|
|
103
|
+
name: 'MCP User',
|
|
104
|
+
email: 'mcp@localhost',
|
|
105
|
+
timestamp: Math.floor(Date.now() / 1000),
|
|
106
|
+
timezoneOffset: new Date().getTimezoneOffset(),
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Convert isomorphic-git status matrix to StatusResult format
|
|
111
|
+
*/
|
|
112
|
+
async convertStatusMatrix(dir, matrix) {
|
|
113
|
+
const FILE = 0, HEAD = 1, WORKDIR = 2, STAGE = 3;
|
|
114
|
+
const modified = [];
|
|
115
|
+
const created = [];
|
|
116
|
+
const deleted = [];
|
|
117
|
+
const not_added = [];
|
|
118
|
+
const files = [];
|
|
119
|
+
for (const row of matrix) {
|
|
120
|
+
const filepath = row[FILE];
|
|
121
|
+
const headStatus = row[HEAD];
|
|
122
|
+
const workdirStatus = row[WORKDIR];
|
|
123
|
+
const stageStatus = row[STAGE];
|
|
124
|
+
// Determine status
|
|
125
|
+
if (headStatus === 0 && workdirStatus === 2 && stageStatus === 0) {
|
|
126
|
+
// New file, not staged
|
|
127
|
+
not_added.push(filepath);
|
|
128
|
+
files.push({ path: filepath, working_dir: 'new' });
|
|
129
|
+
}
|
|
130
|
+
else if (headStatus === 0 && workdirStatus === 2 && stageStatus === 2) {
|
|
131
|
+
// New file, staged
|
|
132
|
+
created.push(filepath);
|
|
133
|
+
files.push({ path: filepath, index: 'new', working_dir: 'new' });
|
|
134
|
+
}
|
|
135
|
+
else if (headStatus === 1 && workdirStatus === 2 && stageStatus === 1) {
|
|
136
|
+
// Modified file, not staged
|
|
137
|
+
not_added.push(filepath);
|
|
138
|
+
files.push({ path: filepath, working_dir: 'modified' });
|
|
139
|
+
}
|
|
140
|
+
else if (headStatus === 1 && workdirStatus === 2 && stageStatus === 2) {
|
|
141
|
+
// Modified file, staged
|
|
142
|
+
modified.push(filepath);
|
|
143
|
+
files.push({ path: filepath, index: 'modified', working_dir: 'modified' });
|
|
144
|
+
}
|
|
145
|
+
else if (headStatus === 1 && workdirStatus === 0 && stageStatus === 1) {
|
|
146
|
+
// Deleted file, not staged
|
|
147
|
+
not_added.push(filepath);
|
|
148
|
+
files.push({ path: filepath, working_dir: 'deleted' });
|
|
149
|
+
}
|
|
150
|
+
else if (headStatus === 1 && workdirStatus === 0 && stageStatus === 0) {
|
|
151
|
+
// Deleted file, staged
|
|
152
|
+
deleted.push(filepath);
|
|
153
|
+
files.push({ path: filepath, index: 'deleted', working_dir: 'deleted' });
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
const current = await this.getCurrentBranch(dir);
|
|
157
|
+
const staged = [...modified, ...created, ...deleted]; // All staged files
|
|
158
|
+
const isClean = modified.length === 0 && created.length === 0 &&
|
|
159
|
+
deleted.length === 0 && not_added.length === 0;
|
|
160
|
+
return {
|
|
161
|
+
modified,
|
|
162
|
+
created,
|
|
163
|
+
deleted,
|
|
164
|
+
renamed: [], // isomorphic-git doesn't detect renames automatically
|
|
165
|
+
not_added,
|
|
166
|
+
staged,
|
|
167
|
+
conflicted: [],
|
|
168
|
+
current,
|
|
169
|
+
tracking: null, // Will be implemented in remote operations
|
|
170
|
+
ahead: 0,
|
|
171
|
+
behind: 0,
|
|
172
|
+
isClean,
|
|
173
|
+
files,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
// ========================================================================
|
|
177
|
+
// REPOSITORY OPERATIONS
|
|
178
|
+
// ========================================================================
|
|
179
|
+
async init(dir, defaultBranch = 'master') {
|
|
180
|
+
await git.init({
|
|
181
|
+
fs,
|
|
182
|
+
dir,
|
|
183
|
+
defaultBranch,
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
async status(dir) {
|
|
187
|
+
const matrix = await git.statusMatrix({
|
|
188
|
+
fs,
|
|
189
|
+
dir,
|
|
190
|
+
});
|
|
191
|
+
return this.convertStatusMatrix(dir, matrix);
|
|
192
|
+
}
|
|
193
|
+
// ========================================================================
|
|
194
|
+
// STAGING OPERATIONS
|
|
195
|
+
// ========================================================================
|
|
196
|
+
async add(dir, files) {
|
|
197
|
+
for (const filepath of files) {
|
|
198
|
+
if (filepath === '.') {
|
|
199
|
+
// Add all files
|
|
200
|
+
const matrix = await git.statusMatrix({ fs, dir });
|
|
201
|
+
for (const row of matrix) {
|
|
202
|
+
const file = row[0];
|
|
203
|
+
const workdirStatus = row[2];
|
|
204
|
+
if (workdirStatus === 2) { // File exists in workdir
|
|
205
|
+
await git.add({ fs, dir, filepath: file });
|
|
206
|
+
}
|
|
207
|
+
else if (workdirStatus === 0 && row[1] === 1) { // File deleted
|
|
208
|
+
await git.remove({ fs, dir, filepath: file });
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
else {
|
|
213
|
+
await git.add({ fs, dir, filepath });
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
async remove(dir, files) {
|
|
218
|
+
for (const filepath of files) {
|
|
219
|
+
await git.remove({ fs, dir, filepath });
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
// ========================================================================
|
|
223
|
+
// COMMIT OPERATIONS
|
|
224
|
+
// ========================================================================
|
|
225
|
+
async commit(dir, message, providedAuthor) {
|
|
226
|
+
const author = await this.getAuthor(dir, providedAuthor);
|
|
227
|
+
const sha = await git.commit({
|
|
228
|
+
fs,
|
|
229
|
+
dir,
|
|
230
|
+
message,
|
|
231
|
+
author: {
|
|
232
|
+
name: author.name,
|
|
233
|
+
email: author.email,
|
|
234
|
+
timestamp: author.timestamp,
|
|
235
|
+
timezoneOffset: author.timezoneOffset,
|
|
236
|
+
},
|
|
237
|
+
});
|
|
238
|
+
return sha;
|
|
239
|
+
}
|
|
240
|
+
// ========================================================================
|
|
241
|
+
// BRANCH OPERATIONS
|
|
242
|
+
// ========================================================================
|
|
243
|
+
async listBranches(dir, remote = false) {
|
|
244
|
+
const branches = remote
|
|
245
|
+
? await git.listBranches({ fs, dir, remote: 'origin' })
|
|
246
|
+
: await git.listBranches({ fs, dir });
|
|
247
|
+
return branches;
|
|
248
|
+
}
|
|
249
|
+
async createBranch(dir, branchName, startPoint) {
|
|
250
|
+
// Simply create branch - isomorphic-git creates it at current HEAD by default
|
|
251
|
+
await git.branch({ fs, dir, ref: branchName });
|
|
252
|
+
}
|
|
253
|
+
async deleteBranch(dir, branchName, force) {
|
|
254
|
+
// Note: isomorphic-git doesn't have a force option for branch deletion
|
|
255
|
+
// We'll implement it by checking if branch is merged
|
|
256
|
+
await git.deleteBranch({ fs, dir, ref: branchName });
|
|
257
|
+
}
|
|
258
|
+
async renameBranch(dir, oldName, newName) {
|
|
259
|
+
// isomorphic-git doesn't have renameBranch, so we rename the ref file directly
|
|
260
|
+
const currentBranch = await this.getCurrentBranch(dir);
|
|
261
|
+
const wasOnOldBranch = currentBranch === oldName;
|
|
262
|
+
// Read the old branch ref
|
|
263
|
+
const oldRefPath = path.join(dir, '.git', 'refs', 'heads', oldName);
|
|
264
|
+
const newRefPath = path.join(dir, '.git', 'refs', 'heads', newName);
|
|
265
|
+
if (!fs.existsSync(oldRefPath)) {
|
|
266
|
+
throw new Error(`Branch ${oldName} does not exist`);
|
|
267
|
+
}
|
|
268
|
+
// Copy ref to new location
|
|
269
|
+
const refContent = fs.readFileSync(oldRefPath, 'utf8');
|
|
270
|
+
fs.writeFileSync(newRefPath, refContent);
|
|
271
|
+
// Delete old ref
|
|
272
|
+
fs.unlinkSync(oldRefPath);
|
|
273
|
+
// If we were on the old branch, update HEAD
|
|
274
|
+
if (wasOnOldBranch) {
|
|
275
|
+
const headPath = path.join(dir, '.git', 'HEAD');
|
|
276
|
+
fs.writeFileSync(headPath, `ref: refs/heads/${newName}\n`);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
async checkout(dir, ref) {
|
|
280
|
+
await git.checkout({
|
|
281
|
+
fs,
|
|
282
|
+
dir,
|
|
283
|
+
ref,
|
|
284
|
+
force: false,
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
async getCurrentBranch(dir) {
|
|
288
|
+
try {
|
|
289
|
+
const branch = await git.currentBranch({ fs, dir, fullname: false });
|
|
290
|
+
return branch || 'HEAD';
|
|
291
|
+
}
|
|
292
|
+
catch (err) {
|
|
293
|
+
return 'HEAD';
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
// ========================================================================
|
|
297
|
+
// REMOTE OPERATIONS (Placeholder - will be implemented in next step)
|
|
298
|
+
// ========================================================================
|
|
299
|
+
async addRemote(dir, name, url) {
|
|
300
|
+
await git.addRemote({ fs, dir, remote: name, url });
|
|
301
|
+
}
|
|
302
|
+
async removeRemote(dir, name) {
|
|
303
|
+
await git.deleteRemote({ fs, dir, remote: name });
|
|
304
|
+
}
|
|
305
|
+
async listRemotes(dir) {
|
|
306
|
+
const remotes = await git.listRemotes({ fs, dir });
|
|
307
|
+
return remotes.map((r) => ({
|
|
308
|
+
name: r.remote,
|
|
309
|
+
remote: r.remote,
|
|
310
|
+
url: r.url,
|
|
311
|
+
fetch: r.url,
|
|
312
|
+
}));
|
|
313
|
+
}
|
|
314
|
+
async fetch(dir, remote, ref) {
|
|
315
|
+
const remotes = await git.listRemotes({ fs, dir });
|
|
316
|
+
const remoteUrl = remotes.find(r => r.remote === remote)?.url || '';
|
|
317
|
+
const onAuth = this.getAuthCallback(remoteUrl);
|
|
318
|
+
await git.fetch({
|
|
319
|
+
fs,
|
|
320
|
+
http,
|
|
321
|
+
dir,
|
|
322
|
+
remote,
|
|
323
|
+
ref,
|
|
324
|
+
onAuth,
|
|
325
|
+
singleBranch: !!ref,
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
async pull(dir, remote, branch) {
|
|
329
|
+
await git.pull({
|
|
330
|
+
fs,
|
|
331
|
+
http,
|
|
332
|
+
dir,
|
|
333
|
+
ref: branch,
|
|
334
|
+
remote,
|
|
335
|
+
onAuth: this.getAuthCallback(remote),
|
|
336
|
+
singleBranch: true,
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
async push(dir, remote, branch, force) {
|
|
340
|
+
// Get remote URL to detect provider
|
|
341
|
+
const remotes = await git.listRemotes({ fs, dir });
|
|
342
|
+
const remoteInfo = remotes.find(r => r.remote === remote);
|
|
343
|
+
if (!remoteInfo) {
|
|
344
|
+
throw new Error(`Remote '${remote}' not found`);
|
|
345
|
+
}
|
|
346
|
+
// Determine authentication based on provider
|
|
347
|
+
let onAuth;
|
|
348
|
+
if (remoteInfo.url.includes('github.com')) {
|
|
349
|
+
// GitHub: token as username, 'x-oauth-basic' as password
|
|
350
|
+
const token = process.env.GITHUB_TOKEN;
|
|
351
|
+
if (token) {
|
|
352
|
+
onAuth = () => ({
|
|
353
|
+
username: token,
|
|
354
|
+
password: 'x-oauth-basic'
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
else if (remoteInfo.url.includes('gitlab.com')) {
|
|
359
|
+
// GitLab: 'oauth2' as username, token as password
|
|
360
|
+
const token = process.env.GITLAB_TOKEN;
|
|
361
|
+
if (token) {
|
|
362
|
+
onAuth = () => ({
|
|
363
|
+
username: 'oauth2',
|
|
364
|
+
password: token
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
else {
|
|
369
|
+
// Gitea and others: username as username, token as password
|
|
370
|
+
const username = process.env.GITEA_USERNAME || 'git';
|
|
371
|
+
const token = process.env.GITEA_TOKEN;
|
|
372
|
+
if (token) {
|
|
373
|
+
onAuth = () => ({
|
|
374
|
+
username: username,
|
|
375
|
+
password: token
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
try {
|
|
380
|
+
// Set upstream tracking automatically
|
|
381
|
+
const currentBranch = await this.getCurrentBranch(dir);
|
|
382
|
+
const refToPush = branch.startsWith('refs/') ? branch : `refs/heads/${branch}`;
|
|
383
|
+
await git.push({
|
|
384
|
+
fs,
|
|
385
|
+
http,
|
|
386
|
+
dir,
|
|
387
|
+
remote,
|
|
388
|
+
ref: refToPush,
|
|
389
|
+
remoteRef: refToPush,
|
|
390
|
+
onAuth,
|
|
391
|
+
force,
|
|
392
|
+
onAuthFailure: () => {
|
|
393
|
+
throw new Error(`Authentication failed for remote '${remote}'`);
|
|
394
|
+
}
|
|
395
|
+
});
|
|
396
|
+
// Set upstream tracking branch configuration
|
|
397
|
+
if (!branch.startsWith('refs/') && currentBranch === branch) {
|
|
398
|
+
await git.setConfig({
|
|
399
|
+
fs,
|
|
400
|
+
dir,
|
|
401
|
+
path: `branch.${branch}.remote`,
|
|
402
|
+
value: remote
|
|
403
|
+
});
|
|
404
|
+
await git.setConfig({
|
|
405
|
+
fs,
|
|
406
|
+
dir,
|
|
407
|
+
path: `branch.${branch}.merge`,
|
|
408
|
+
value: `refs/heads/${branch}`
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
catch (error) {
|
|
413
|
+
if (error.code === 'HTTP401Unauthorized' || error.code === 'HTTP403Forbidden') {
|
|
414
|
+
throw new Error(`Push failed: Authentication error for ${remote}. ` +
|
|
415
|
+
`Check your token and permissions. Error: ${error.message}`);
|
|
416
|
+
}
|
|
417
|
+
throw error;
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
// ========================================================================
|
|
421
|
+
// MERGE OPERATIONS (Placeholder - will be implemented in next step)
|
|
422
|
+
// ========================================================================
|
|
423
|
+
async merge(dir, theirBranch, author) {
|
|
424
|
+
const commitAuthor = await this.getAuthor(dir, author);
|
|
425
|
+
const currentBranch = await this.getCurrentBranch(dir);
|
|
426
|
+
try {
|
|
427
|
+
await git.merge({
|
|
428
|
+
fs,
|
|
429
|
+
dir,
|
|
430
|
+
ours: currentBranch,
|
|
431
|
+
theirs: theirBranch,
|
|
432
|
+
author: {
|
|
433
|
+
name: commitAuthor.name,
|
|
434
|
+
email: commitAuthor.email,
|
|
435
|
+
},
|
|
436
|
+
});
|
|
437
|
+
// Atualizar working directory com os arquivos merged
|
|
438
|
+
await git.checkout({
|
|
439
|
+
fs,
|
|
440
|
+
dir,
|
|
441
|
+
ref: currentBranch,
|
|
442
|
+
force: true,
|
|
443
|
+
});
|
|
444
|
+
return {
|
|
445
|
+
success: true,
|
|
446
|
+
message: `Merged ${theirBranch} into ${currentBranch}`,
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
catch (err) {
|
|
450
|
+
if (err.code === 'MergeNotSupportedError') {
|
|
451
|
+
return {
|
|
452
|
+
success: false,
|
|
453
|
+
conflicts: err.data?.conflicts || [],
|
|
454
|
+
message: `Merge conflict detected. Files with conflicts: ${err.data?.conflicts?.join(', ') || 'unknown'}. Please resolve conflicts manually.`,
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
throw err;
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
// ========================================================================
|
|
461
|
+
// TAG OPERATIONS (Placeholder - will be implemented in next step)
|
|
462
|
+
// ========================================================================
|
|
463
|
+
async listTags(dir) {
|
|
464
|
+
const tags = await git.listTags({ fs, dir });
|
|
465
|
+
return tags;
|
|
466
|
+
}
|
|
467
|
+
async createTag(dir, tagName, ref = 'HEAD', message) {
|
|
468
|
+
const oid = await git.resolveRef({ fs, dir, ref });
|
|
469
|
+
if (message) {
|
|
470
|
+
const author = await this.getAuthor(dir);
|
|
471
|
+
await git.annotatedTag({
|
|
472
|
+
fs,
|
|
473
|
+
dir,
|
|
474
|
+
ref: tagName,
|
|
475
|
+
object: oid,
|
|
476
|
+
message,
|
|
477
|
+
tagger: {
|
|
478
|
+
name: author.name,
|
|
479
|
+
email: author.email,
|
|
480
|
+
timestamp: author.timestamp,
|
|
481
|
+
timezoneOffset: author.timezoneOffset,
|
|
482
|
+
},
|
|
483
|
+
});
|
|
484
|
+
}
|
|
485
|
+
else {
|
|
486
|
+
await git.tag({
|
|
487
|
+
fs,
|
|
488
|
+
dir,
|
|
489
|
+
ref: tagName,
|
|
490
|
+
object: oid,
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
async deleteTag(dir, tagName) {
|
|
495
|
+
await git.deleteTag({ fs, dir, ref: tagName });
|
|
496
|
+
}
|
|
497
|
+
// ========================================================================
|
|
498
|
+
// HISTORY OPERATIONS (Placeholder - will be implemented in next step)
|
|
499
|
+
// ========================================================================
|
|
500
|
+
async log(dir, options) {
|
|
501
|
+
const commits = await git.log({
|
|
502
|
+
fs,
|
|
503
|
+
dir,
|
|
504
|
+
ref: options?.ref || 'HEAD',
|
|
505
|
+
depth: options?.maxCount,
|
|
506
|
+
});
|
|
507
|
+
return commits.map(commit => ({
|
|
508
|
+
hash: commit.oid,
|
|
509
|
+
date: new Date(commit.commit.author.timestamp * 1000).toISOString(),
|
|
510
|
+
message: commit.commit.message,
|
|
511
|
+
author_name: commit.commit.author.name,
|
|
512
|
+
author_email: commit.commit.author.email,
|
|
513
|
+
refs: '', // Will be populated if needed
|
|
514
|
+
}));
|
|
515
|
+
}
|
|
516
|
+
async diff(dir, ref1, ref2) {
|
|
517
|
+
// Basic diff implementation - will be enhanced with walkers
|
|
518
|
+
return {
|
|
519
|
+
files: [],
|
|
520
|
+
summary: {
|
|
521
|
+
additions: 0,
|
|
522
|
+
deletions: 0,
|
|
523
|
+
changes: 0,
|
|
524
|
+
},
|
|
525
|
+
};
|
|
526
|
+
}
|
|
527
|
+
// ========================================================================
|
|
528
|
+
// RESET OPERATIONS (Placeholder - will be implemented in next step)
|
|
529
|
+
// ========================================================================
|
|
530
|
+
async reset(dir, ref, mode) {
|
|
531
|
+
// Resolve ref - handle HEAD~N syntax manually since isomorphic-git doesn't support it
|
|
532
|
+
let oid;
|
|
533
|
+
if (ref.match(/^HEAD~\d+$/)) {
|
|
534
|
+
// Extract number from HEAD~N
|
|
535
|
+
const steps = parseInt(ref.replace('HEAD~', ''));
|
|
536
|
+
const logs = await git.log({ fs, dir, depth: steps + 1 });
|
|
537
|
+
if (logs.length <= steps) {
|
|
538
|
+
throw new Error(`Not enough commits to resolve ${ref}`);
|
|
539
|
+
}
|
|
540
|
+
oid = logs[steps].oid;
|
|
541
|
+
}
|
|
542
|
+
else {
|
|
543
|
+
oid = await git.resolveRef({ fs, dir, ref });
|
|
544
|
+
}
|
|
545
|
+
const currentBranch = await this.getCurrentBranch(dir);
|
|
546
|
+
if (mode === 'hard') {
|
|
547
|
+
// Hard reset: move HEAD, update index, and update working directory
|
|
548
|
+
await git.writeRef({ fs, dir, ref: `refs/heads/${currentBranch}`, value: oid, force: true });
|
|
549
|
+
await git.checkout({ fs, dir, ref: oid, force: true });
|
|
550
|
+
}
|
|
551
|
+
else if (mode === 'mixed') {
|
|
552
|
+
// Mixed reset: move HEAD and update index, keep working directory
|
|
553
|
+
await git.writeRef({ fs, dir, ref: `refs/heads/${currentBranch}`, value: oid, force: true });
|
|
554
|
+
await git.checkout({ fs, dir, ref: oid });
|
|
555
|
+
}
|
|
556
|
+
else {
|
|
557
|
+
// Soft reset: only move HEAD
|
|
558
|
+
await git.writeRef({ fs, dir, ref: `refs/heads/${currentBranch}`, value: oid, force: true });
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
// ========================================================================
|
|
562
|
+
// STASH OPERATIONS - Hybrid implementation using refs/stash
|
|
563
|
+
// ========================================================================
|
|
564
|
+
/**
|
|
565
|
+
* Get stash reflog path
|
|
566
|
+
*/
|
|
567
|
+
getStashReflogPath(dir) {
|
|
568
|
+
return path.join(dir, '.git', 'logs', 'refs', 'stash');
|
|
569
|
+
}
|
|
570
|
+
/**
|
|
571
|
+
* Get stash ref path
|
|
572
|
+
*/
|
|
573
|
+
getStashRefPath(dir) {
|
|
574
|
+
return path.join(dir, '.git', 'refs', 'stash');
|
|
575
|
+
}
|
|
576
|
+
/**
|
|
577
|
+
* Read stash reflog entries
|
|
578
|
+
*/
|
|
579
|
+
async readStashReflog(dir) {
|
|
580
|
+
const reflogPath = this.getStashReflogPath(dir);
|
|
581
|
+
if (!fs.existsSync(reflogPath)) {
|
|
582
|
+
return [];
|
|
583
|
+
}
|
|
584
|
+
const content = fs.readFileSync(reflogPath, 'utf8');
|
|
585
|
+
const lines = content.trim().split('\n').filter(l => l);
|
|
586
|
+
const entries = [];
|
|
587
|
+
for (let i = 0; i < lines.length; i++) {
|
|
588
|
+
const line = lines[i];
|
|
589
|
+
const match = line.match(/^(\w+) (\w+) .+ <.+> (\d+) [+-]\d+ (.+)$/);
|
|
590
|
+
if (match) {
|
|
591
|
+
const [, , newSha, timestamp, msg] = match;
|
|
592
|
+
entries.push({
|
|
593
|
+
index: i,
|
|
594
|
+
message: msg.replace(/^stash@\{\d+\}: /, ''),
|
|
595
|
+
date: new Date(parseInt(timestamp) * 1000),
|
|
596
|
+
ref: newSha,
|
|
597
|
+
});
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
return entries.reverse(); // Most recent first
|
|
601
|
+
}
|
|
602
|
+
/**
|
|
603
|
+
* Write stash reflog entry
|
|
604
|
+
*/
|
|
605
|
+
async writeStashReflogEntry(dir, oldSha, newSha, message) {
|
|
606
|
+
const reflogPath = this.getStashReflogPath(dir);
|
|
607
|
+
const reflogDir = path.dirname(reflogPath);
|
|
608
|
+
// Create logs/refs directory if it doesn't exist
|
|
609
|
+
if (!fs.existsSync(reflogDir)) {
|
|
610
|
+
fs.mkdirSync(reflogDir, { recursive: true });
|
|
611
|
+
}
|
|
612
|
+
const author = await this.getAuthor(dir);
|
|
613
|
+
const timestamp = Math.floor(Date.now() / 1000);
|
|
614
|
+
const timezoneOffset = new Date().getTimezoneOffset();
|
|
615
|
+
const tzString = timezoneOffset <= 0
|
|
616
|
+
? `+${String(Math.abs(timezoneOffset) / 60).padStart(2, '0')}00`
|
|
617
|
+
: `-${String(timezoneOffset / 60).padStart(2, '0')}00`;
|
|
618
|
+
const entry = `${oldSha} ${newSha} ${author.name} <${author.email}> ${timestamp} ${tzString} ${message}\n`;
|
|
619
|
+
fs.appendFileSync(reflogPath, entry);
|
|
620
|
+
}
|
|
621
|
+
async stashSave(dir, message, includeUntracked) {
|
|
622
|
+
const author = await this.getAuthor(dir);
|
|
623
|
+
const currentBranch = await this.getCurrentBranch(dir);
|
|
624
|
+
const stashMessage = message || `WIP on ${currentBranch}`;
|
|
625
|
+
// Get current HEAD
|
|
626
|
+
const headSha = await git.resolveRef({ fs, dir, ref: 'HEAD' });
|
|
627
|
+
// Create index commit (staged changes)
|
|
628
|
+
const matrix = await git.statusMatrix({ fs, dir });
|
|
629
|
+
const stagedFiles = [];
|
|
630
|
+
for (const row of matrix) {
|
|
631
|
+
const filepath = row[0];
|
|
632
|
+
const stageStatus = row[3];
|
|
633
|
+
if (stageStatus === 2) {
|
|
634
|
+
stagedFiles.push(filepath);
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
// Stage all changes for stash commit
|
|
638
|
+
for (const row of matrix) {
|
|
639
|
+
const filepath = row[0];
|
|
640
|
+
const workdirStatus = row[2];
|
|
641
|
+
if (workdirStatus === 2) {
|
|
642
|
+
await git.add({ fs, dir, filepath });
|
|
643
|
+
}
|
|
644
|
+
else if (workdirStatus === 0 && row[1] === 1) {
|
|
645
|
+
await git.remove({ fs, dir, filepath });
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
// Create stash commit
|
|
649
|
+
const stashSha = await git.commit({
|
|
650
|
+
fs,
|
|
651
|
+
dir,
|
|
652
|
+
message: `stash@{0}: ${stashMessage}`,
|
|
653
|
+
author: {
|
|
654
|
+
name: author.name,
|
|
655
|
+
email: author.email,
|
|
656
|
+
timestamp: author.timestamp,
|
|
657
|
+
timezoneOffset: author.timezoneOffset,
|
|
658
|
+
},
|
|
659
|
+
parent: [headSha],
|
|
660
|
+
});
|
|
661
|
+
// Update refs/stash
|
|
662
|
+
const stashRefPath = this.getStashRefPath(dir);
|
|
663
|
+
const stashRefDir = path.dirname(stashRefPath);
|
|
664
|
+
if (!fs.existsSync(stashRefDir)) {
|
|
665
|
+
fs.mkdirSync(stashRefDir, { recursive: true });
|
|
666
|
+
}
|
|
667
|
+
fs.writeFileSync(stashRefPath, stashSha + '\n');
|
|
668
|
+
// Write reflog entry
|
|
669
|
+
const oldStashSha = fs.existsSync(stashRefPath) ? fs.readFileSync(stashRefPath, 'utf8').trim() : '0000000000000000000000000000000000000000';
|
|
670
|
+
await this.writeStashReflogEntry(dir, oldStashSha, stashSha, `stash@{0}: ${stashMessage}`);
|
|
671
|
+
// Reset working directory to HEAD
|
|
672
|
+
await git.checkout({ fs, dir, ref: headSha, force: true });
|
|
673
|
+
// Restore originally staged files
|
|
674
|
+
for (const filepath of stagedFiles) {
|
|
675
|
+
await git.add({ fs, dir, filepath });
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
async stashList(dir) {
|
|
679
|
+
return await this.readStashReflog(dir);
|
|
680
|
+
}
|
|
681
|
+
async stashApply(dir, stashRef) {
|
|
682
|
+
const stashes = await this.readStashReflog(dir);
|
|
683
|
+
if (stashes.length === 0) {
|
|
684
|
+
throw new Error('No stash entries found');
|
|
685
|
+
}
|
|
686
|
+
let stashEntry;
|
|
687
|
+
if (stashRef) {
|
|
688
|
+
const match = stashRef.match(/stash@\{(\d+)\}/);
|
|
689
|
+
const index = match ? parseInt(match[1]) : 0;
|
|
690
|
+
stashEntry = stashes[index];
|
|
691
|
+
if (!stashEntry) {
|
|
692
|
+
throw new Error(`Stash entry not found: ${stashRef}`);
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
else {
|
|
696
|
+
stashEntry = stashes[0]; // Most recent
|
|
697
|
+
}
|
|
698
|
+
// Read commit to get files
|
|
699
|
+
const commit = await git.readCommit({ fs, dir, oid: stashEntry.ref });
|
|
700
|
+
// Checkout files from stash commit
|
|
701
|
+
await git.checkout({
|
|
702
|
+
fs,
|
|
703
|
+
dir,
|
|
704
|
+
ref: stashEntry.ref,
|
|
705
|
+
force: false,
|
|
706
|
+
filepaths: undefined,
|
|
707
|
+
});
|
|
708
|
+
}
|
|
709
|
+
async stashPop(dir, stashRef) {
|
|
710
|
+
await this.stashApply(dir, stashRef);
|
|
711
|
+
await this.stashDrop(dir, stashRef);
|
|
712
|
+
}
|
|
713
|
+
async stashDrop(dir, stashRef) {
|
|
714
|
+
const stashes = await this.readStashReflog(dir);
|
|
715
|
+
if (stashes.length === 0) {
|
|
716
|
+
throw new Error('No stash entries found');
|
|
717
|
+
}
|
|
718
|
+
let indexToDrop = 0;
|
|
719
|
+
if (stashRef) {
|
|
720
|
+
const match = stashRef.match(/stash@\{(\d+)\}/);
|
|
721
|
+
indexToDrop = match ? parseInt(match[1]) : 0;
|
|
722
|
+
}
|
|
723
|
+
if (indexToDrop >= stashes.length) {
|
|
724
|
+
throw new Error(`Stash entry not found: ${stashRef || 'stash@{0}'}`);
|
|
725
|
+
}
|
|
726
|
+
// Rewrite reflog without the dropped entry
|
|
727
|
+
const reflogPath = this.getStashReflogPath(dir);
|
|
728
|
+
const content = fs.readFileSync(reflogPath, 'utf8');
|
|
729
|
+
const lines = content.trim().split('\n').filter(l => l);
|
|
730
|
+
lines.splice(lines.length - 1 - indexToDrop, 1);
|
|
731
|
+
if (lines.length > 0) {
|
|
732
|
+
fs.writeFileSync(reflogPath, lines.join('\n') + '\n');
|
|
733
|
+
// Update refs/stash to point to the new top
|
|
734
|
+
const newTopLine = lines[lines.length - 1];
|
|
735
|
+
const match = newTopLine.match(/^(\w+) (\w+)/);
|
|
736
|
+
if (match) {
|
|
737
|
+
fs.writeFileSync(this.getStashRefPath(dir), match[2] + '\n');
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
else {
|
|
741
|
+
// No more stashes, remove ref and reflog
|
|
742
|
+
if (fs.existsSync(this.getStashRefPath(dir))) {
|
|
743
|
+
fs.unlinkSync(this.getStashRefPath(dir));
|
|
744
|
+
}
|
|
745
|
+
if (fs.existsSync(reflogPath)) {
|
|
746
|
+
fs.unlinkSync(reflogPath);
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
async stashClear(dir) {
|
|
751
|
+
const reflogPath = this.getStashReflogPath(dir);
|
|
752
|
+
const refPath = this.getStashRefPath(dir);
|
|
753
|
+
if (fs.existsSync(reflogPath)) {
|
|
754
|
+
fs.unlinkSync(reflogPath);
|
|
755
|
+
}
|
|
756
|
+
if (fs.existsSync(refPath)) {
|
|
757
|
+
fs.unlinkSync(refPath);
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
// ========================================================================
|
|
761
|
+
// CONFIG OPERATIONS - Hybrid implementation (local + global/system)
|
|
762
|
+
// ========================================================================
|
|
763
|
+
/**
|
|
764
|
+
* Get global Git config path (~/.gitconfig)
|
|
765
|
+
*/
|
|
766
|
+
getGlobalConfigPath() {
|
|
767
|
+
const homeDir = process.env.HOME || process.env.USERPROFILE || '';
|
|
768
|
+
return path.join(homeDir, '.gitconfig');
|
|
769
|
+
}
|
|
770
|
+
/**
|
|
771
|
+
* Get system Git config path
|
|
772
|
+
*/
|
|
773
|
+
getSystemConfigPath() {
|
|
774
|
+
if (process.platform === 'win32') {
|
|
775
|
+
const programData = process.env.PROGRAMDATA || 'C:\\ProgramData';
|
|
776
|
+
return path.join(programData, 'Git', 'config');
|
|
777
|
+
}
|
|
778
|
+
else {
|
|
779
|
+
return '/etc/gitconfig';
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
/**
|
|
783
|
+
* Parse INI-style Git config file
|
|
784
|
+
*/
|
|
785
|
+
parseGitConfig(content) {
|
|
786
|
+
const config = {};
|
|
787
|
+
const lines = content.split('\n');
|
|
788
|
+
let currentSection = '';
|
|
789
|
+
let currentSubsection = '';
|
|
790
|
+
for (const line of lines) {
|
|
791
|
+
const trimmed = line.trim();
|
|
792
|
+
// Skip empty lines and comments
|
|
793
|
+
if (!trimmed || trimmed.startsWith('#') || trimmed.startsWith(';')) {
|
|
794
|
+
continue;
|
|
795
|
+
}
|
|
796
|
+
// Match section headers: [section] or [section "subsection"]
|
|
797
|
+
const sectionMatch = trimmed.match(/^\[([^\]"]+)(?:\s+"([^"]+)")?\]$/);
|
|
798
|
+
if (sectionMatch) {
|
|
799
|
+
currentSection = sectionMatch[1];
|
|
800
|
+
currentSubsection = sectionMatch[2] || '';
|
|
801
|
+
continue;
|
|
802
|
+
}
|
|
803
|
+
// Match key-value pairs
|
|
804
|
+
const keyValueMatch = trimmed.match(/^([^=]+?)\s*=\s*(.*)$/);
|
|
805
|
+
if (keyValueMatch && currentSection) {
|
|
806
|
+
const key = keyValueMatch[1].trim();
|
|
807
|
+
const value = keyValueMatch[2].trim().replace(/^"|"$/g, ''); // Remove quotes
|
|
808
|
+
let fullKey = currentSection;
|
|
809
|
+
if (currentSubsection) {
|
|
810
|
+
fullKey += `.${currentSubsection}`;
|
|
811
|
+
}
|
|
812
|
+
fullKey += `.${key}`;
|
|
813
|
+
config[fullKey] = value;
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
return config;
|
|
817
|
+
}
|
|
818
|
+
/**
|
|
819
|
+
* Write INI-style Git config file
|
|
820
|
+
*/
|
|
821
|
+
writeGitConfig(config) {
|
|
822
|
+
const sections = {};
|
|
823
|
+
// Group by section
|
|
824
|
+
for (const [fullKey, value] of Object.entries(config)) {
|
|
825
|
+
const parts = fullKey.split('.');
|
|
826
|
+
if (parts.length >= 2) {
|
|
827
|
+
const section = parts.slice(0, -1).join('.');
|
|
828
|
+
const key = parts[parts.length - 1];
|
|
829
|
+
if (!sections[section]) {
|
|
830
|
+
sections[section] = {};
|
|
831
|
+
}
|
|
832
|
+
sections[section][key] = value;
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
// Generate INI content
|
|
836
|
+
let content = '';
|
|
837
|
+
for (const [section, keys] of Object.entries(sections)) {
|
|
838
|
+
// Handle subsections: core.remote "origin" -> [core "origin"]
|
|
839
|
+
const sectionParts = section.split('.');
|
|
840
|
+
if (sectionParts.length > 1) {
|
|
841
|
+
content += `[${sectionParts[0]} "${sectionParts.slice(1).join('.')}"]\n`;
|
|
842
|
+
}
|
|
843
|
+
else {
|
|
844
|
+
content += `[${section}]\n`;
|
|
845
|
+
}
|
|
846
|
+
for (const [key, value] of Object.entries(keys)) {
|
|
847
|
+
if (value) { // Skip empty values
|
|
848
|
+
content += `\t${key} = ${value}\n`;
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
return content;
|
|
853
|
+
}
|
|
854
|
+
async getConfig(dir, key, scope) {
|
|
855
|
+
if (scope === 'global') {
|
|
856
|
+
const globalConfigPath = this.getGlobalConfigPath();
|
|
857
|
+
if (fs.existsSync(globalConfigPath)) {
|
|
858
|
+
const content = fs.readFileSync(globalConfigPath, 'utf8');
|
|
859
|
+
const config = this.parseGitConfig(content);
|
|
860
|
+
return config[key];
|
|
861
|
+
}
|
|
862
|
+
return undefined;
|
|
863
|
+
}
|
|
864
|
+
else if (scope === 'system') {
|
|
865
|
+
const systemConfigPath = this.getSystemConfigPath();
|
|
866
|
+
if (fs.existsSync(systemConfigPath)) {
|
|
867
|
+
const content = fs.readFileSync(systemConfigPath, 'utf8');
|
|
868
|
+
const config = this.parseGitConfig(content);
|
|
869
|
+
return config[key];
|
|
870
|
+
}
|
|
871
|
+
return undefined;
|
|
872
|
+
}
|
|
873
|
+
else {
|
|
874
|
+
// Local config via isomorphic-git
|
|
875
|
+
return await git.getConfig({ fs, dir, path: key });
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
async setConfig(dir, key, value, scope) {
|
|
879
|
+
if (scope === 'global') {
|
|
880
|
+
const globalConfigPath = this.getGlobalConfigPath();
|
|
881
|
+
let config = {};
|
|
882
|
+
if (fs.existsSync(globalConfigPath)) {
|
|
883
|
+
const content = fs.readFileSync(globalConfigPath, 'utf8');
|
|
884
|
+
config = this.parseGitConfig(content);
|
|
885
|
+
}
|
|
886
|
+
config[key] = value;
|
|
887
|
+
const newContent = this.writeGitConfig(config);
|
|
888
|
+
fs.writeFileSync(globalConfigPath, newContent);
|
|
889
|
+
}
|
|
890
|
+
else if (scope === 'system') {
|
|
891
|
+
const systemConfigPath = this.getSystemConfigPath();
|
|
892
|
+
let config = {};
|
|
893
|
+
if (fs.existsSync(systemConfigPath)) {
|
|
894
|
+
const content = fs.readFileSync(systemConfigPath, 'utf8');
|
|
895
|
+
config = this.parseGitConfig(content);
|
|
896
|
+
}
|
|
897
|
+
config[key] = value;
|
|
898
|
+
const newContent = this.writeGitConfig(config);
|
|
899
|
+
fs.writeFileSync(systemConfigPath, newContent);
|
|
900
|
+
}
|
|
901
|
+
else {
|
|
902
|
+
// Local config via isomorphic-git
|
|
903
|
+
await git.setConfig({ fs, dir, path: key, value });
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
async listConfig(dir, scope) {
|
|
907
|
+
if (scope === 'global') {
|
|
908
|
+
const globalConfigPath = this.getGlobalConfigPath();
|
|
909
|
+
if (fs.existsSync(globalConfigPath)) {
|
|
910
|
+
const content = fs.readFileSync(globalConfigPath, 'utf8');
|
|
911
|
+
return this.parseGitConfig(content);
|
|
912
|
+
}
|
|
913
|
+
return {};
|
|
914
|
+
}
|
|
915
|
+
else if (scope === 'system') {
|
|
916
|
+
const systemConfigPath = this.getSystemConfigPath();
|
|
917
|
+
if (fs.existsSync(systemConfigPath)) {
|
|
918
|
+
const content = fs.readFileSync(systemConfigPath, 'utf8');
|
|
919
|
+
return this.parseGitConfig(content);
|
|
920
|
+
}
|
|
921
|
+
return {};
|
|
922
|
+
}
|
|
923
|
+
else {
|
|
924
|
+
// Local config - read .git/config directly
|
|
925
|
+
const configPath = path.join(dir, '.git', 'config');
|
|
926
|
+
if (fs.existsSync(configPath)) {
|
|
927
|
+
const content = fs.readFileSync(configPath, 'utf8');
|
|
928
|
+
return this.parseGitConfig(content);
|
|
929
|
+
}
|
|
930
|
+
return {};
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
// Alias for backwards compatibility
|
|
934
|
+
async getAllConfig(dir, scope) {
|
|
935
|
+
return this.listConfig(dir, scope);
|
|
936
|
+
}
|
|
937
|
+
async unsetConfig(dir, key, scope) {
|
|
938
|
+
if (scope === 'global') {
|
|
939
|
+
const globalConfigPath = this.getGlobalConfigPath();
|
|
940
|
+
if (fs.existsSync(globalConfigPath)) {
|
|
941
|
+
const content = fs.readFileSync(globalConfigPath, 'utf8');
|
|
942
|
+
const config = this.parseGitConfig(content);
|
|
943
|
+
delete config[key];
|
|
944
|
+
const newContent = this.writeGitConfig(config);
|
|
945
|
+
fs.writeFileSync(globalConfigPath, newContent);
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
else if (scope === 'system') {
|
|
949
|
+
const systemConfigPath = this.getSystemConfigPath();
|
|
950
|
+
if (fs.existsSync(systemConfigPath)) {
|
|
951
|
+
const content = fs.readFileSync(systemConfigPath, 'utf8');
|
|
952
|
+
const config = this.parseGitConfig(content);
|
|
953
|
+
delete config[key];
|
|
954
|
+
const newContent = this.writeGitConfig(config);
|
|
955
|
+
fs.writeFileSync(systemConfigPath, newContent);
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
else {
|
|
959
|
+
// Local config - isomorphic-git doesn't have unsetConfig
|
|
960
|
+
// We'll read, modify, and write the .git/config file
|
|
961
|
+
const configPath = path.join(dir, '.git', 'config');
|
|
962
|
+
if (fs.existsSync(configPath)) {
|
|
963
|
+
const content = fs.readFileSync(configPath, 'utf8');
|
|
964
|
+
const config = this.parseGitConfig(content);
|
|
965
|
+
delete config[key];
|
|
966
|
+
const newContent = this.writeGitConfig(config);
|
|
967
|
+
fs.writeFileSync(configPath, newContent);
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
// ========================================================================
|
|
972
|
+
// RESET CONVENIENCE METHODS
|
|
973
|
+
// ========================================================================
|
|
974
|
+
async resetSoft(dir, ref) {
|
|
975
|
+
return this.reset(dir, ref, 'soft');
|
|
976
|
+
}
|
|
977
|
+
async resetMixed(dir, ref) {
|
|
978
|
+
return this.reset(dir, ref, 'mixed');
|
|
979
|
+
}
|
|
980
|
+
async resetHard(dir, ref) {
|
|
981
|
+
return this.reset(dir, ref, 'hard');
|
|
982
|
+
}
|
|
983
|
+
// ========================================================================
|
|
984
|
+
// GITIGNORE MANAGEMENT
|
|
985
|
+
// ========================================================================
|
|
986
|
+
async createGitignore(dir, patterns) {
|
|
987
|
+
const gitignorePath = path.join(dir, '.gitignore');
|
|
988
|
+
fs.writeFileSync(gitignorePath, patterns.join('\n') + '\n');
|
|
989
|
+
}
|
|
990
|
+
async addToGitignore(dir, patterns) {
|
|
991
|
+
const gitignorePath = path.join(dir, '.gitignore');
|
|
992
|
+
let existing = '';
|
|
993
|
+
if (fs.existsSync(gitignorePath)) {
|
|
994
|
+
existing = fs.readFileSync(gitignorePath, 'utf8');
|
|
995
|
+
}
|
|
996
|
+
const newPatterns = patterns.filter(p => !existing.includes(p));
|
|
997
|
+
if (newPatterns.length > 0) {
|
|
998
|
+
fs.appendFileSync(gitignorePath, '\n' + newPatterns.join('\n') + '\n');
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
async removeFromGitignore(dir, patterns) {
|
|
1002
|
+
const gitignorePath = path.join(dir, '.gitignore');
|
|
1003
|
+
if (!fs.existsSync(gitignorePath))
|
|
1004
|
+
return;
|
|
1005
|
+
let content = fs.readFileSync(gitignorePath, 'utf8');
|
|
1006
|
+
const lines = content.split('\n');
|
|
1007
|
+
const filtered = lines.filter(line => !patterns.includes(line.trim()));
|
|
1008
|
+
fs.writeFileSync(gitignorePath, filtered.join('\n'));
|
|
1009
|
+
}
|
|
1010
|
+
async listGitignore(dir) {
|
|
1011
|
+
const gitignorePath = path.join(dir, '.gitignore');
|
|
1012
|
+
if (!fs.existsSync(gitignorePath))
|
|
1013
|
+
return [];
|
|
1014
|
+
const content = fs.readFileSync(gitignorePath, 'utf8');
|
|
1015
|
+
return content.split('\n')
|
|
1016
|
+
.map(line => line.trim())
|
|
1017
|
+
.filter(line => line && !line.startsWith('#'));
|
|
1018
|
+
}
|
|
1019
|
+
// ========================================================================
|
|
1020
|
+
// FILE OPERATIONS
|
|
1021
|
+
// ========================================================================
|
|
1022
|
+
async readFile(dir, filepath, ref = 'HEAD') {
|
|
1023
|
+
try {
|
|
1024
|
+
const oid = await git.resolveRef({ fs, dir, ref });
|
|
1025
|
+
const { blob } = await git.readBlob({
|
|
1026
|
+
fs,
|
|
1027
|
+
dir,
|
|
1028
|
+
oid,
|
|
1029
|
+
filepath,
|
|
1030
|
+
});
|
|
1031
|
+
return Buffer.from(blob).toString('utf8');
|
|
1032
|
+
}
|
|
1033
|
+
catch (err) {
|
|
1034
|
+
// Fallback to reading from working directory
|
|
1035
|
+
const fullPath = path.join(dir, filepath);
|
|
1036
|
+
if (fs.existsSync(fullPath)) {
|
|
1037
|
+
return fs.readFileSync(fullPath, 'utf8');
|
|
1038
|
+
}
|
|
1039
|
+
throw new Error(`File not found: ${filepath}`);
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
async listFiles(dir, ref = 'HEAD') {
|
|
1043
|
+
try {
|
|
1044
|
+
const oid = await git.resolveRef({ fs, dir, ref });
|
|
1045
|
+
const tree = await git.listFiles({ fs, dir, ref: oid });
|
|
1046
|
+
return tree;
|
|
1047
|
+
}
|
|
1048
|
+
catch (err) {
|
|
1049
|
+
// Fallback to reading working directory
|
|
1050
|
+
const files = [];
|
|
1051
|
+
const walk = (currentPath, relativePath = '') => {
|
|
1052
|
+
const entries = fs.readdirSync(currentPath, { withFileTypes: true });
|
|
1053
|
+
for (const entry of entries) {
|
|
1054
|
+
if (entry.name === '.git')
|
|
1055
|
+
continue;
|
|
1056
|
+
const fullPath = path.join(currentPath, entry.name);
|
|
1057
|
+
const relPath = path.join(relativePath, entry.name);
|
|
1058
|
+
if (entry.isDirectory()) {
|
|
1059
|
+
walk(fullPath, relPath);
|
|
1060
|
+
}
|
|
1061
|
+
else {
|
|
1062
|
+
files.push(relPath.replace(/\\/g, '/'));
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
};
|
|
1066
|
+
walk(dir);
|
|
1067
|
+
return files;
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
//# sourceMappingURL=gitAdapter.js.map
|