@andrebuzeli/git-mcp 15.8.4 → 15.8.5
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 +39 -125
- package/package.json +27 -44
- package/src/index.js +146 -139
- package/src/providers/providerManager.js +203 -217
- package/src/tools/git-diff.js +137 -126
- package/src/tools/git-help.js +285 -285
- package/src/tools/git-remote.js +472 -472
- package/src/tools/git-workflow.js +403 -403
- package/src/utils/env.js +104 -104
- package/src/utils/errors.js +431 -431
- package/src/utils/gitAdapter.js +932 -951
- package/src/utils/hooks.js +255 -255
- package/src/utils/metrics.js +198 -198
- package/src/utils/providerExec.js +58 -58
- package/src/utils/repoHelpers.js +160 -160
- package/src/utils/retry.js +123 -123
- package/install.sh +0 -68
package/src/utils/gitAdapter.js
CHANGED
|
@@ -1,951 +1,932 @@
|
|
|
1
|
-
import { spawn } from "node:child_process";
|
|
2
|
-
import fs from "node:fs";
|
|
3
|
-
import path from "node:path";
|
|
4
|
-
import { MCPError, createError, mapExternalError } from "./errors.js";
|
|
5
|
-
import { getRepoNameFromPath, getProvidersEnv } from "./repoHelpers.js";
|
|
6
|
-
|
|
7
|
-
// Common locations for Git on Windows, Linux, and macOS
|
|
8
|
-
const GIT_CANDIDATES = [
|
|
9
|
-
// Windows paths
|
|
10
|
-
"C:\\Program Files\\Git\\mingw64\\bin\\git.exe",
|
|
11
|
-
"C:\\Program Files\\Git\\bin\\git.exe",
|
|
12
|
-
"C:\\Program Files\\Git\\cmd\\git.exe",
|
|
13
|
-
"C:\\Users\\andre\\AppData\\Local\\Programs\\Git\\mingw64\\bin\\git.exe",
|
|
14
|
-
"C:\\Users\\andre\\AppData\\Local\\Programs\\Git\\bin\\git.exe",
|
|
15
|
-
"C:\\Users\\andre\\AppData\\Local\\Programs\\Git\\cmd\\git.exe",
|
|
16
|
-
// Linux/macOS paths
|
|
17
|
-
"/usr/bin/git",
|
|
18
|
-
"/usr/local/bin/git",
|
|
19
|
-
"/opt/homebrew/bin/git", // macOS Homebrew ARM
|
|
20
|
-
// Fallback to PATH
|
|
21
|
-
"git"
|
|
22
|
-
];
|
|
23
|
-
|
|
24
|
-
// Timeout configurável via variável de ambiente (default: 60 segundos)
|
|
25
|
-
const GIT_TIMEOUT = parseInt(process.env.GIT_TIMEOUT_MS || "60000", 10);
|
|
26
|
-
|
|
27
|
-
export class GitAdapter {
|
|
28
|
-
constructor(providerManager) {
|
|
29
|
-
this.pm = providerManager;
|
|
30
|
-
this.gitPath = "git"; // Initial default
|
|
31
|
-
this.gitEnv = { ...process.env, LANG: "en_US.UTF-8" }; // Base env
|
|
32
|
-
this.timeout = GIT_TIMEOUT;
|
|
33
|
-
this.resolvePromise = this._resolveGit();
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
async _resolveGit() {
|
|
37
|
-
console.error("[GitAdapter] Resolving git binary...");
|
|
38
|
-
|
|
39
|
-
for (const candidate of GIT_CANDIDATES) {
|
|
40
|
-
if (candidate !== "git" && !fs.existsSync(candidate)) continue;
|
|
41
|
-
|
|
42
|
-
try {
|
|
43
|
-
const version = await this._runSpawn(candidate, ["--version"], ".");
|
|
44
|
-
console.error(`[GitAdapter] Found git at: ${candidate} (${version.trim()})`);
|
|
45
|
-
this.gitPath = candidate;
|
|
46
|
-
|
|
47
|
-
// If we found a specific binary, we might need to adjust PATH for standard utils (ssh, etc)
|
|
48
|
-
// But for simply running git, the absolute path is key.
|
|
49
|
-
return;
|
|
50
|
-
} catch (e) {
|
|
51
|
-
// console.error(`[GitAdapter] Failed candidate ${candidate}: ${e.message}`);
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
console.error("[GitAdapter] WARNING: Could not resolve specific git binary. Using 'git' from PATH.");
|
|
56
|
-
this.gitPath = "git";
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
// Low-level spawn wrapper with timeout
|
|
60
|
-
_runSpawn(cmd, args, dir, env = {}) {
|
|
61
|
-
return new Promise((resolve, reject) => {
|
|
62
|
-
const cp = spawn(cmd, args, {
|
|
63
|
-
cwd: dir,
|
|
64
|
-
env: {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
cp.on("
|
|
88
|
-
clearTimeout(timeoutId);
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
if (
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
const
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
//
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
await this.
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
const
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
const
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
const
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
await this.
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
}
|
|
514
|
-
}
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
if (
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
const
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
}
|
|
606
|
-
|
|
607
|
-
async
|
|
608
|
-
const
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
async
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
}
|
|
625
|
-
|
|
626
|
-
async
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
}
|
|
636
|
-
|
|
637
|
-
async
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
const
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
}
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
const
|
|
677
|
-
out.
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
}
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
const
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
const
|
|
696
|
-
const
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
}
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
const
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
}
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
/**
|
|
865
|
-
*
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
await this._exec(dir, ["lfs", "pull"]);
|
|
934
|
-
}
|
|
935
|
-
|
|
936
|
-
/**
|
|
937
|
-
* Envia arquivos LFS
|
|
938
|
-
*/
|
|
939
|
-
async lfsPush(dir, remote = "origin") {
|
|
940
|
-
await this._exec(dir, ["lfs", "push", "--all", remote]);
|
|
941
|
-
}
|
|
942
|
-
|
|
943
|
-
/**
|
|
944
|
-
* Status do Git LFS
|
|
945
|
-
*/
|
|
946
|
-
async lfsStatus(dir) {
|
|
947
|
-
const out = await this._exec(dir, ["lfs", "status"]);
|
|
948
|
-
return out;
|
|
949
|
-
}
|
|
950
|
-
}
|
|
951
|
-
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { MCPError, createError, mapExternalError } from "./errors.js";
|
|
5
|
+
import { getRepoNameFromPath, getProvidersEnv } from "./repoHelpers.js";
|
|
6
|
+
|
|
7
|
+
// Common locations for Git on Windows, Linux, and macOS
|
|
8
|
+
const GIT_CANDIDATES = [
|
|
9
|
+
// Windows paths
|
|
10
|
+
"C:\\Program Files\\Git\\mingw64\\bin\\git.exe",
|
|
11
|
+
"C:\\Program Files\\Git\\bin\\git.exe",
|
|
12
|
+
"C:\\Program Files\\Git\\cmd\\git.exe",
|
|
13
|
+
"C:\\Users\\andre\\AppData\\Local\\Programs\\Git\\mingw64\\bin\\git.exe",
|
|
14
|
+
"C:\\Users\\andre\\AppData\\Local\\Programs\\Git\\bin\\git.exe",
|
|
15
|
+
"C:\\Users\\andre\\AppData\\Local\\Programs\\Git\\cmd\\git.exe",
|
|
16
|
+
// Linux/macOS paths
|
|
17
|
+
"/usr/bin/git",
|
|
18
|
+
"/usr/local/bin/git",
|
|
19
|
+
"/opt/homebrew/bin/git", // macOS Homebrew ARM
|
|
20
|
+
// Fallback to PATH
|
|
21
|
+
"git"
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
// Timeout configurável via variável de ambiente (default: 60 segundos)
|
|
25
|
+
const GIT_TIMEOUT = parseInt(process.env.GIT_TIMEOUT_MS || "60000", 10);
|
|
26
|
+
|
|
27
|
+
export class GitAdapter {
|
|
28
|
+
constructor(providerManager) {
|
|
29
|
+
this.pm = providerManager;
|
|
30
|
+
this.gitPath = "git"; // Initial default
|
|
31
|
+
this.gitEnv = { ...process.env, LANG: "en_US.UTF-8" }; // Base env
|
|
32
|
+
this.timeout = GIT_TIMEOUT;
|
|
33
|
+
this.resolvePromise = this._resolveGit();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async _resolveGit() {
|
|
37
|
+
console.error("[GitAdapter] Resolving git binary...");
|
|
38
|
+
|
|
39
|
+
for (const candidate of GIT_CANDIDATES) {
|
|
40
|
+
if (candidate !== "git" && !fs.existsSync(candidate)) continue;
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
const version = await this._runSpawn(candidate, ["--version"], ".");
|
|
44
|
+
console.error(`[GitAdapter] Found git at: ${candidate} (${version.trim()})`);
|
|
45
|
+
this.gitPath = candidate;
|
|
46
|
+
|
|
47
|
+
// If we found a specific binary, we might need to adjust PATH for standard utils (ssh, etc)
|
|
48
|
+
// But for simply running git, the absolute path is key.
|
|
49
|
+
return;
|
|
50
|
+
} catch (e) {
|
|
51
|
+
// console.error(`[GitAdapter] Failed candidate ${candidate}: ${e.message}`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
console.error("[GitAdapter] WARNING: Could not resolve specific git binary. Using 'git' from PATH.");
|
|
56
|
+
this.gitPath = "git";
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Low-level spawn wrapper with timeout
|
|
60
|
+
_runSpawn(cmd, args, dir, env = {}) {
|
|
61
|
+
return new Promise((resolve, reject) => {
|
|
62
|
+
const cp = spawn(cmd, args, {
|
|
63
|
+
cwd: dir,
|
|
64
|
+
env: { ...this.gitEnv, ...env },
|
|
65
|
+
stdio: 'pipe'
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
let stdout = [];
|
|
69
|
+
let stderr = [];
|
|
70
|
+
let killed = false;
|
|
71
|
+
|
|
72
|
+
// Timeout handler
|
|
73
|
+
const timeoutId = setTimeout(() => {
|
|
74
|
+
killed = true;
|
|
75
|
+
cp.kill('SIGTERM');
|
|
76
|
+
reject(new Error(`Git command timed out after ${this.timeout}ms: git ${args.join(" ")}`));
|
|
77
|
+
}, this.timeout);
|
|
78
|
+
|
|
79
|
+
cp.stdout.on("data", d => stdout.push(d));
|
|
80
|
+
cp.stderr.on("data", d => stderr.push(d));
|
|
81
|
+
|
|
82
|
+
cp.on("error", e => {
|
|
83
|
+
clearTimeout(timeoutId);
|
|
84
|
+
reject(new Error(`Failed to spawn ${cmd}: ${e.message}`));
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
cp.on("close", code => {
|
|
88
|
+
clearTimeout(timeoutId);
|
|
89
|
+
if (killed) return; // Already rejected by timeout
|
|
90
|
+
|
|
91
|
+
const outStr = Buffer.concat(stdout).toString("utf8").trim();
|
|
92
|
+
const errStr = Buffer.concat(stderr).toString("utf8").trim();
|
|
93
|
+
|
|
94
|
+
if (code === 0) {
|
|
95
|
+
resolve(outStr);
|
|
96
|
+
} else {
|
|
97
|
+
const msg = errStr || outStr || `Exit code ${code}`;
|
|
98
|
+
reject(new Error(msg));
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async _exec(dir, args, options = {}) {
|
|
105
|
+
await this.resolvePromise;
|
|
106
|
+
|
|
107
|
+
try {
|
|
108
|
+
const output = await this._runSpawn(this.gitPath, args, dir, options.env);
|
|
109
|
+
return output;
|
|
110
|
+
} catch (e) {
|
|
111
|
+
const msg = e.message || "";
|
|
112
|
+
|
|
113
|
+
// Auto-fix 1: dubious ownership error (common on network shares)
|
|
114
|
+
if (msg.includes("dubious ownership") && !options._retried) {
|
|
115
|
+
console.error("[GitAdapter] Auto-fix: dubious ownership, adding safe.directory...");
|
|
116
|
+
try {
|
|
117
|
+
await this._runSpawn(this.gitPath, ["config", "--global", "--add", "safe.directory", "*"], ".");
|
|
118
|
+
return await this._exec(dir, args, { ...options, _retried: true });
|
|
119
|
+
} catch (configError) {
|
|
120
|
+
console.error("[GitAdapter] Failed to configure safe.directory:", configError.message);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Auto-fix 2: Lock file presente (outro processo git pode ter crashado)
|
|
125
|
+
if ((msg.includes(".git/index.lock") || msg.includes("Unable to create") && msg.includes("lock")) && !options._lockRetried) {
|
|
126
|
+
const lockPath = path.join(dir, ".git", "index.lock");
|
|
127
|
+
if (fs.existsSync(lockPath)) {
|
|
128
|
+
console.error("[GitAdapter] Auto-fix: removing stale .git/index.lock...");
|
|
129
|
+
try {
|
|
130
|
+
fs.unlinkSync(lockPath);
|
|
131
|
+
return await this._exec(dir, args, { ...options, _lockRetried: true });
|
|
132
|
+
} catch (unlinkError) {
|
|
133
|
+
console.error("[GitAdapter] Failed to remove lock file:", unlinkError.message);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Auto-fix 3: CRLF warnings (just retry with autocrlf config)
|
|
139
|
+
if (msg.includes("CRLF will be replaced") && !options._crlfRetried) {
|
|
140
|
+
console.error("[GitAdapter] Auto-fix: configuring core.autocrlf...");
|
|
141
|
+
try {
|
|
142
|
+
await this._runSpawn(this.gitPath, ["config", "--local", "core.autocrlf", "true"], dir);
|
|
143
|
+
return await this._exec(dir, args, { ...options, _crlfRetried: true });
|
|
144
|
+
} catch {
|
|
145
|
+
// Continue with error
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (options.ignoreErrors) return "";
|
|
150
|
+
|
|
151
|
+
// Enhance error message with context
|
|
152
|
+
const cmdStr = `git ${args.join(" ")}`;
|
|
153
|
+
const enhancedError = new Error(`Command failed: ${cmdStr}\n${msg}`);
|
|
154
|
+
enhancedError.gitCommand = args[0];
|
|
155
|
+
enhancedError.originalMessage = msg;
|
|
156
|
+
throw enhancedError;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
async _ensureGitRepo(dir) {
|
|
161
|
+
if (!fs.existsSync(path.join(dir, ".git"))) {
|
|
162
|
+
throw createError("NOT_A_GIT_REPO", {
|
|
163
|
+
message: `'${dir}' não é um repositório git`,
|
|
164
|
+
suggestion: "Use git-workflow init para criar um repositório"
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Verifica se o diretório é um repositório git
|
|
171
|
+
* @param {string} dir - Diretório para verificar
|
|
172
|
+
* @returns {boolean} - true se é um repo git
|
|
173
|
+
*/
|
|
174
|
+
async isRepo(dir) {
|
|
175
|
+
return fs.existsSync(path.join(dir, ".git"));
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Verifica integridade do repositório git
|
|
180
|
+
* @param {string} dir - Diretório do repositório
|
|
181
|
+
* @returns {object} - Status de integridade
|
|
182
|
+
*/
|
|
183
|
+
async checkIntegrity(dir) {
|
|
184
|
+
await this._ensureGitRepo(dir);
|
|
185
|
+
|
|
186
|
+
try {
|
|
187
|
+
// Verificação rápida de objetos
|
|
188
|
+
const output = await this._exec(dir, ["fsck", "--connectivity-only"], { ignoreErrors: true });
|
|
189
|
+
const hasErrors = output.includes("error") || output.includes("missing") || output.includes("broken");
|
|
190
|
+
|
|
191
|
+
return {
|
|
192
|
+
ok: !hasErrors,
|
|
193
|
+
output: output || "Repository integrity OK",
|
|
194
|
+
suggestion: hasErrors ? "Execute 'git gc --prune=now' ou considere re-clonar o repositório" : null
|
|
195
|
+
};
|
|
196
|
+
} catch (e) {
|
|
197
|
+
return {
|
|
198
|
+
ok: false,
|
|
199
|
+
error: e.message,
|
|
200
|
+
suggestion: "Repositório pode estar corrompido. Tente: git gc --prune=now ou re-clone"
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Valida formato de URL de remote
|
|
207
|
+
* @param {string} url - URL para validar
|
|
208
|
+
* @returns {object} - Resultado da validação
|
|
209
|
+
*/
|
|
210
|
+
validateRemoteUrl(url) {
|
|
211
|
+
if (!url || typeof url !== "string") {
|
|
212
|
+
return { valid: false, error: "URL vazia ou inválida" };
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Padrões válidos de URL git
|
|
216
|
+
const patterns = [
|
|
217
|
+
/^https?:\/\/[^\s]+\.git$/i, // https://github.com/user/repo.git
|
|
218
|
+
/^https?:\/\/[^\s]+$/i, // https://github.com/user/repo (sem .git)
|
|
219
|
+
/^git@[^\s]+:[^\s]+\.git$/i, // git@github.com:user/repo.git
|
|
220
|
+
/^git:\/\/[^\s]+\.git$/i, // git://github.com/user/repo.git
|
|
221
|
+
/^ssh:\/\/[^\s]+\.git$/i, // ssh://git@github.com/user/repo.git
|
|
222
|
+
];
|
|
223
|
+
|
|
224
|
+
const isValid = patterns.some(p => p.test(url));
|
|
225
|
+
|
|
226
|
+
if (!isValid) {
|
|
227
|
+
return {
|
|
228
|
+
valid: false,
|
|
229
|
+
error: "Formato de URL inválido",
|
|
230
|
+
suggestion: "Use: https://github.com/user/repo.git ou git@github.com:user/repo.git"
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return { valid: true, url };
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// =================================================================
|
|
238
|
+
// Git Operations
|
|
239
|
+
// =================================================================
|
|
240
|
+
|
|
241
|
+
async init(dir, defaultBranch = "main") {
|
|
242
|
+
await fs.promises.mkdir(dir, { recursive: true });
|
|
243
|
+
// Try modern init
|
|
244
|
+
try {
|
|
245
|
+
await this._exec(dir, ["init", "-b", defaultBranch]);
|
|
246
|
+
} catch {
|
|
247
|
+
await this._exec(dir, ["init"]);
|
|
248
|
+
await this._exec(dir, ["symbolic-ref", "HEAD", `refs/heads/${defaultBranch}`]).catch(() => { });
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
async status(dir) {
|
|
253
|
+
await this._ensureGitRepo(dir);
|
|
254
|
+
|
|
255
|
+
// porcelain v1 is easy to parse
|
|
256
|
+
const out = await this._exec(dir, ["status", "--porcelain=v1"]);
|
|
257
|
+
const lines = out.split("\n").filter(Boolean);
|
|
258
|
+
|
|
259
|
+
const modified = [], created = [], deleted = [], notAdded = [], staged = [], files = [];
|
|
260
|
+
|
|
261
|
+
for (const line of lines) {
|
|
262
|
+
// XY PATH
|
|
263
|
+
const code = line.substring(0, 2);
|
|
264
|
+
const file = line.substring(3).trim();
|
|
265
|
+
const x = code[0], y = code[1];
|
|
266
|
+
|
|
267
|
+
const fileInfo = { path: file };
|
|
268
|
+
|
|
269
|
+
// X = Index
|
|
270
|
+
if (x !== ' ' && x !== '?') {
|
|
271
|
+
staged.push(file);
|
|
272
|
+
if (x === 'A') created.push(file);
|
|
273
|
+
if (x === 'D') deleted.push(file);
|
|
274
|
+
fileInfo.index = x === 'A' ? "new" : x === 'D' ? "deleted" : "modified";
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Y = Worktree
|
|
278
|
+
if (y !== ' ') {
|
|
279
|
+
if (y === 'M') { modified.push(file); fileInfo.workingDir = "modified"; }
|
|
280
|
+
if (y === 'D') { notAdded.push(file); fileInfo.workingDir = "deleted"; }
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Untracked
|
|
284
|
+
if (code === '??') {
|
|
285
|
+
notAdded.push(file);
|
|
286
|
+
fileInfo.workingDir = "new";
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
files.push(fileInfo);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const currentBranch = await this.getCurrentBranch(dir);
|
|
293
|
+
return {
|
|
294
|
+
modified,
|
|
295
|
+
created,
|
|
296
|
+
deleted,
|
|
297
|
+
renamed: [],
|
|
298
|
+
notAdded,
|
|
299
|
+
// Aliases para compatibilidade
|
|
300
|
+
not_added: notAdded,
|
|
301
|
+
staged,
|
|
302
|
+
conflicted: [],
|
|
303
|
+
currentBranch,
|
|
304
|
+
current: currentBranch, // Alias para compatibilidade
|
|
305
|
+
isClean: lines.length === 0,
|
|
306
|
+
files
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
async add(dir, files) {
|
|
311
|
+
if (files.includes(".")) {
|
|
312
|
+
await this._exec(dir, ["add", "."]);
|
|
313
|
+
await this._exec(dir, ["add", "--all"]); // Catch deletions if . didn't
|
|
314
|
+
} else {
|
|
315
|
+
await this._exec(dir, ["add", ...files]);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
async remove(dir, files) {
|
|
320
|
+
await this._exec(dir, ["rm", "--cached", ...files]);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
async getAuthor(dir) {
|
|
324
|
+
try {
|
|
325
|
+
const name = await this._exec(dir, ["config", "user.name"]);
|
|
326
|
+
const email = await this._exec(dir, ["config", "user.email"]);
|
|
327
|
+
if (name && email) return { name, email };
|
|
328
|
+
} catch { }
|
|
329
|
+
|
|
330
|
+
// Fallback logic
|
|
331
|
+
const ownerGH = await this.pm.getGitHubOwner().catch(() => null);
|
|
332
|
+
const ownerGE = await this.pm.getGiteaOwner().catch(() => null);
|
|
333
|
+
const who = ownerGH || ownerGE || "Git User";
|
|
334
|
+
const email = `${who}@users.noreply`;
|
|
335
|
+
|
|
336
|
+
await this.setConfig(dir, "user.name", who);
|
|
337
|
+
await this.setConfig(dir, "user.email", email);
|
|
338
|
+
|
|
339
|
+
return { name: who, email };
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
async commit(dir, message) {
|
|
343
|
+
await this.getAuthor(dir);
|
|
344
|
+
await this._exec(dir, ["commit", "-m", message]);
|
|
345
|
+
return await this._exec(dir, ["rev-parse", "HEAD"]);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
async getCurrentBranch(dir) {
|
|
349
|
+
try {
|
|
350
|
+
return await this._exec(dir, ["rev-parse", "--abbrev-ref", "HEAD"]);
|
|
351
|
+
} catch {
|
|
352
|
+
return "HEAD";
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// ============ REMOTES ============
|
|
357
|
+
async ensureRemotes(dir, { githubUrl, giteaUrl }) {
|
|
358
|
+
const remotesOutput = await this._exec(dir, ["remote", "-v"]);
|
|
359
|
+
const remotes = new Set(remotesOutput.split("\n").map(l => l.split("\t")[0]).filter(Boolean));
|
|
360
|
+
|
|
361
|
+
const repoName = getRepoNameFromPath(dir);
|
|
362
|
+
const calcUrls = await this.pm.getRemoteUrls(repoName);
|
|
363
|
+
const targetGithub = calcUrls.github || githubUrl;
|
|
364
|
+
const targetGitea = calcUrls.gitea || giteaUrl;
|
|
365
|
+
|
|
366
|
+
const setRemote = async (name, url) => {
|
|
367
|
+
if (!url) return;
|
|
368
|
+
if (remotes.has(name)) {
|
|
369
|
+
await this._exec(dir, ["remote", "set-url", name, url]);
|
|
370
|
+
} else {
|
|
371
|
+
await this._exec(dir, ["remote", "add", name, url]);
|
|
372
|
+
}
|
|
373
|
+
};
|
|
374
|
+
|
|
375
|
+
await setRemote("github", targetGithub);
|
|
376
|
+
await setRemote("gitea", targetGitea);
|
|
377
|
+
|
|
378
|
+
const originUrl = targetGithub || targetGitea;
|
|
379
|
+
if (originUrl) await setRemote("origin", originUrl);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
_getAuthHeader(url) {
|
|
383
|
+
const { githubToken, giteaToken } = getProvidersEnv();
|
|
384
|
+
if (url.includes("github.com") && githubToken) {
|
|
385
|
+
const basic = Buffer.from(`${githubToken}:x-oauth-basic`).toString("base64");
|
|
386
|
+
return `Authorization: Basic ${basic}`;
|
|
387
|
+
}
|
|
388
|
+
if (giteaToken) {
|
|
389
|
+
const basic = Buffer.from(`git:${giteaToken}`).toString("base64");
|
|
390
|
+
return `Authorization: Basic ${basic}`;
|
|
391
|
+
}
|
|
392
|
+
return null;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
async pushOne(dir, remote, branch, force = false, setUpstream = false) {
|
|
396
|
+
const remoteUrl = await this._exec(dir, ["remote", "get-url", remote]);
|
|
397
|
+
const header = this._getAuthHeader(remoteUrl);
|
|
398
|
+
|
|
399
|
+
const args = [];
|
|
400
|
+
if (header) {
|
|
401
|
+
args.push("-c", `http.extraHeader=${header}`);
|
|
402
|
+
}
|
|
403
|
+
args.push("push");
|
|
404
|
+
if (force) args.push("--force");
|
|
405
|
+
if (setUpstream) {
|
|
406
|
+
args.push("-u", remote, branch);
|
|
407
|
+
} else {
|
|
408
|
+
args.push(remote, branch);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
try {
|
|
412
|
+
await this._exec(dir, args);
|
|
413
|
+
} catch (e) {
|
|
414
|
+
const msg = e.message || "";
|
|
415
|
+
const msgLower = msg.toLowerCase();
|
|
416
|
+
|
|
417
|
+
// Auto-correção: Se branch não existe no remote, tenta com -u (set-upstream)
|
|
418
|
+
const branchNotExists = msgLower.includes("has no upstream branch") ||
|
|
419
|
+
msgLower.includes("no upstream branch") ||
|
|
420
|
+
(msgLower.includes("remote branch") && msgLower.includes("does not exist")) ||
|
|
421
|
+
msgLower.includes("ref_not_found") ||
|
|
422
|
+
(msgLower.includes("fatal") && msgLower.includes("current branch") && msgLower.includes("has no upstream"));
|
|
423
|
+
|
|
424
|
+
if (branchNotExists && !setUpstream && !force) {
|
|
425
|
+
console.error(`[GitAdapter] Auto-fix: branch '${branch}' não existe no remote '${remote}', tentando com --set-upstream (-u)...`);
|
|
426
|
+
return await this.pushOne(dir, remote, branch, force, true);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
if (msg.includes("rejected") || msgLower.includes("non-fast-forward")) {
|
|
430
|
+
throw createError("PUSH_REJECTED", { message: msg, remote, branch });
|
|
431
|
+
}
|
|
432
|
+
throw mapExternalError(e, { type: "push", remote, branch });
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
async pushParallel(dir, branch, force = false) {
|
|
437
|
+
await this.ensureRemotes(dir, {});
|
|
438
|
+
const remotesStr = await this._exec(dir, ["remote"]);
|
|
439
|
+
const remotes = remotesStr.split("\n").filter(r => ["github", "gitea"].includes(r.trim()));
|
|
440
|
+
|
|
441
|
+
if (remotes.length === 0) {
|
|
442
|
+
throw createError("REMOTE_NOT_FOUND", { message: "Nenhum remote github/gitea configurado" });
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// Parallel push com Promise.allSettled para melhor controle e auto-correção
|
|
446
|
+
const pushPromises = remotes.map(async (remote) => {
|
|
447
|
+
try {
|
|
448
|
+
await this.pushOne(dir, remote, branch, force, false);
|
|
449
|
+
return { remote, success: true, setUpstream: false };
|
|
450
|
+
} catch (error) {
|
|
451
|
+
const errorMsg = error.message || String(error);
|
|
452
|
+
const errorMsgLower = errorMsg.toLowerCase();
|
|
453
|
+
|
|
454
|
+
// Auto-correção 1: Se repositório não existe, cria automaticamente
|
|
455
|
+
const repoNotFound = errorMsgLower.includes("repository not found") ||
|
|
456
|
+
errorMsgLower.includes("repo not found") ||
|
|
457
|
+
(errorMsgLower.includes("fatal") && errorMsgLower.includes("repository") && errorMsgLower.includes("not found")) ||
|
|
458
|
+
(errorMsgLower.includes("not found") && (errorMsgLower.includes("remote") || errorMsgLower.includes("404")));
|
|
459
|
+
|
|
460
|
+
if (repoNotFound && !force) {
|
|
461
|
+
try {
|
|
462
|
+
console.error(`[GitAdapter] Auto-fix: repositório não existe no remote '${remote}', criando automaticamente...`);
|
|
463
|
+
const repoName = getRepoNameFromPath(dir);
|
|
464
|
+
const ensured = await this.pm.ensureRepos({ repoName, createIfMissing: true, isPublic: false });
|
|
465
|
+
|
|
466
|
+
// Atualiza remotes após criar repo
|
|
467
|
+
await this.ensureRemotes(dir, {});
|
|
468
|
+
|
|
469
|
+
// Tenta push novamente
|
|
470
|
+
await this.pushOne(dir, remote, branch, force, true); // Usa -u para criar branch também
|
|
471
|
+
return { remote, success: true, repoCreated: true, setUpstream: true };
|
|
472
|
+
} catch (repoCreateError) {
|
|
473
|
+
return { remote, success: false, error: `Falhou ao criar repo: ${repoCreateError.message}`, triedCreateRepo: true };
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// Auto-correção 2: Se branch não existe no remote, tenta com --set-upstream
|
|
478
|
+
const branchNotExists = errorMsgLower.includes("has no upstream branch") ||
|
|
479
|
+
errorMsgLower.includes("no upstream branch") ||
|
|
480
|
+
(errorMsgLower.includes("remote branch") && errorMsgLower.includes("does not exist")) ||
|
|
481
|
+
errorMsgLower.includes("ref_not_found");
|
|
482
|
+
|
|
483
|
+
if (branchNotExists && !force) {
|
|
484
|
+
try {
|
|
485
|
+
console.error(`[GitAdapter] Auto-fix: branch '${branch}' não existe no remote '${remote}', tentando com --set-upstream...`);
|
|
486
|
+
await this.pushOne(dir, remote, branch, force, true);
|
|
487
|
+
return { remote, success: true, setUpstream: true };
|
|
488
|
+
} catch (retryError) {
|
|
489
|
+
return { remote, success: false, error: retryError.message, triedSetUpstream: true };
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
return { remote, success: false, error: errorMsg };
|
|
494
|
+
}
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
const results = await Promise.allSettled(pushPromises);
|
|
498
|
+
const successful = results.filter(r => r.status === "fulfilled" && r.value.success);
|
|
499
|
+
const failed = results.filter(r => r.status === "rejected" || !r.value.success);
|
|
500
|
+
|
|
501
|
+
if (successful.length === 0) {
|
|
502
|
+
throw createError("PUSH_REJECTED", {
|
|
503
|
+
message: "Push falhou para todos os remotes",
|
|
504
|
+
errors: failed.map(f => f.value?.error || f.reason?.message || "Erro desconhecido")
|
|
505
|
+
});
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
return {
|
|
509
|
+
pushed: successful.map(s => s.value.remote),
|
|
510
|
+
failed: failed.map(f => ({
|
|
511
|
+
remote: f.value?.remote || "unknown",
|
|
512
|
+
error: f.value?.error || f.reason?.message || "Erro desconhecido"
|
|
513
|
+
}))
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
// ============ BRANCHES/TAGS ============
|
|
518
|
+
async listBranches(dir, remote = false) {
|
|
519
|
+
const args = ["branch", "--format=%(refname:short)"];
|
|
520
|
+
if (remote) args.push("-r");
|
|
521
|
+
const out = await this._exec(dir, args);
|
|
522
|
+
return out.split("\n").filter(Boolean).map(b => b.trim());
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
async diffStats(dir, from = "HEAD", to) {
|
|
526
|
+
// git diff --shortstat from..to
|
|
527
|
+
const args = ["diff", "--shortstat", from];
|
|
528
|
+
if (to) args.push(to);
|
|
529
|
+
|
|
530
|
+
try {
|
|
531
|
+
const out = await this._exec(dir, args);
|
|
532
|
+
// Result: " 1 file changed, 1 insertion(+), 1 deletion(-)"
|
|
533
|
+
const filesChanged = (out.match(/(\d+) file(s?) changed/) || [])[1] || 0;
|
|
534
|
+
const insertions = (out.match(/(\d+) insertion(s?)/) || [])[1] || 0;
|
|
535
|
+
const deletions = (out.match(/(\d+) deletion(s?)/) || [])[1] || 0;
|
|
536
|
+
|
|
537
|
+
return {
|
|
538
|
+
filesChanged: parseInt(filesChanged),
|
|
539
|
+
insertions: parseInt(insertions),
|
|
540
|
+
deletions: parseInt(deletions)
|
|
541
|
+
};
|
|
542
|
+
} catch {
|
|
543
|
+
return { filesChanged: 0, insertions: 0, deletions: 0 };
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
async createBranch(dir, name) {
|
|
548
|
+
await this._exec(dir, ["branch", name]);
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
async deleteBranch(dir, name) {
|
|
552
|
+
try {
|
|
553
|
+
await this._exec(dir, ["branch", "-D", name]);
|
|
554
|
+
} catch (e) {
|
|
555
|
+
throw createError("BRANCH_NOT_FOUND", { message: e.message });
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
async renameBranch(dir, oldName, newName) {
|
|
560
|
+
await this._exec(dir, ["branch", "-m", oldName, newName]);
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
async checkout(dir, ref) {
|
|
564
|
+
try {
|
|
565
|
+
await this._exec(dir, ["checkout", ref]);
|
|
566
|
+
} catch (e) {
|
|
567
|
+
throw createError("REF_NOT_FOUND", { message: e.message });
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
async listTags(dir) {
|
|
572
|
+
const out = await this._exec(dir, ["tag"]);
|
|
573
|
+
return out.split("\n").filter(Boolean);
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
async createTag(dir, tag, ref = "HEAD", message = "") {
|
|
577
|
+
if (message) {
|
|
578
|
+
await this._exec(dir, ["tag", "-a", tag, ref, "-m", message]);
|
|
579
|
+
} else {
|
|
580
|
+
await this._exec(dir, ["tag", tag, ref]);
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
async deleteTag(dir, tag) {
|
|
585
|
+
await this._exec(dir, ["tag", "-d", tag]);
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
async pushTag(dir, remote, tag) {
|
|
589
|
+
const url = await this._exec(dir, ["remote", "get-url", remote]);
|
|
590
|
+
const header = this._getAuthHeader(url);
|
|
591
|
+
const args = [];
|
|
592
|
+
if (header) args.push("-c", `http.extraHeader=${header}`);
|
|
593
|
+
args.push("push", remote, tag);
|
|
594
|
+
|
|
595
|
+
await this._exec(dir, args);
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
// ============ STASH (Native) ============
|
|
599
|
+
async listStash(dir) {
|
|
600
|
+
const out = await this._exec(dir, ["stash", "list", "--pretty=format:%gd: %gs"]);
|
|
601
|
+
return out.split("\n").filter(Boolean).map((line, i) => ({
|
|
602
|
+
index: i,
|
|
603
|
+
message: line.split(":").slice(1).join(":").trim()
|
|
604
|
+
}));
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
async saveStash(dir, message = "WIP", includeUntracked = false) {
|
|
608
|
+
const args = ["stash", "push"];
|
|
609
|
+
if (includeUntracked) args.push("-u");
|
|
610
|
+
args.push("-m", message);
|
|
611
|
+
await this._exec(dir, args);
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
async applyStash(dir, ref = "stash@{0}") {
|
|
615
|
+
await this._exec(dir, ["stash", "apply", ref]);
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
async popStash(dir, ref = "stash@{0}") {
|
|
619
|
+
await this._exec(dir, ["stash", "pop", ref]);
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
async dropStash(dir, ref = "stash@{0}") {
|
|
623
|
+
await this._exec(dir, ["stash", "drop", ref]);
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
async clearStash(dir) {
|
|
627
|
+
await this._exec(dir, ["stash", "clear"]);
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
// ============ CONFIG ============
|
|
631
|
+
async getConfig(dir, key) {
|
|
632
|
+
try {
|
|
633
|
+
return await this._exec(dir, ["config", "--get", key]);
|
|
634
|
+
} catch { return undefined; }
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
async setConfig(dir, key, value, scope = "local") {
|
|
638
|
+
const args = ["config"];
|
|
639
|
+
if (scope === "global") args.push("--global");
|
|
640
|
+
else if (scope === "system") args.push("--system");
|
|
641
|
+
else args.push("--local"); // default
|
|
642
|
+
args.push(key, value);
|
|
643
|
+
await this._exec(dir, args);
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
async unsetConfig(dir, key, scope = "local") {
|
|
647
|
+
const args = ["config"];
|
|
648
|
+
if (scope === "global") args.push("--global");
|
|
649
|
+
else if (scope === "system") args.push("--system");
|
|
650
|
+
else args.push("--local");
|
|
651
|
+
args.push("--unset", key);
|
|
652
|
+
try { await this._exec(dir, args); } catch { }
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
async listConfig(dir) {
|
|
656
|
+
const out = await this._exec(dir, ["config", "--list"]);
|
|
657
|
+
const items = {};
|
|
658
|
+
out.split("\n").filter(Boolean).forEach(line => {
|
|
659
|
+
const [k, ...v] = line.split("=");
|
|
660
|
+
items[k] = v.join("=");
|
|
661
|
+
});
|
|
662
|
+
return items;
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
// ============ FILES/LOG ============
|
|
666
|
+
async listFiles(dir, ref = "HEAD") {
|
|
667
|
+
const out = await this._exec(dir, ["ls-tree", "-r", "--name-only", ref]);
|
|
668
|
+
return out.split("\n").filter(Boolean);
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
async readFile(dir, filepath, ref = "HEAD") {
|
|
672
|
+
return await this._exec(dir, ["show", `${ref}:${filepath}`]);
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
async log(dir, { ref = "HEAD", maxCount = 50 } = {}) {
|
|
676
|
+
const format = "%H|%h|%s|%an|%ae|%aI";
|
|
677
|
+
const out = await this._exec(dir, ["log", "-n", maxCount.toString(), `--pretty=format:${format}`, ref]);
|
|
678
|
+
return out.split("\n").filter(Boolean).map(line => {
|
|
679
|
+
const [sha, short, message, name, email, date] = line.split("|");
|
|
680
|
+
return { sha, message, author: { name, email, timestamp: new Date(date).getTime() / 1000 }, date };
|
|
681
|
+
});
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
async fetch(dir, remote, branch) {
|
|
685
|
+
const url = await this._exec(dir, ["remote", "get-url", remote]);
|
|
686
|
+
const header = this._getAuthHeader(url);
|
|
687
|
+
const args = [];
|
|
688
|
+
if (header) args.push("-c", `http.extraHeader=${header}`);
|
|
689
|
+
args.push("fetch", remote, branch);
|
|
690
|
+
await this._exec(dir, args);
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
async pull(dir, remote, branch) {
|
|
694
|
+
const url = await this._exec(dir, ["remote", "get-url", remote]);
|
|
695
|
+
const header = this._getAuthHeader(url);
|
|
696
|
+
const args = [];
|
|
697
|
+
if (header) args.push("-c", `http.extraHeader=${header}`);
|
|
698
|
+
args.push("pull", remote, branch);
|
|
699
|
+
|
|
700
|
+
try {
|
|
701
|
+
await this._exec(dir, args);
|
|
702
|
+
} catch (e) {
|
|
703
|
+
if (e.message.includes("conflict")) throw createError("MERGE_CONFLICT", { message: "Conflict on pull" });
|
|
704
|
+
throw e;
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
// ============ RESET/MERGE ============
|
|
709
|
+
async resetSoft(dir, ref) { await this._exec(dir, ["reset", "--soft", ref]); }
|
|
710
|
+
async resetMixed(dir, ref) { await this._exec(dir, ["reset", "--mixed", ref]); }
|
|
711
|
+
async resetHard(dir, ref) { await this._exec(dir, ["reset", "--hard", ref]); }
|
|
712
|
+
|
|
713
|
+
async resetHardClean(dir, ref) {
|
|
714
|
+
await this._exec(dir, ["reset", "--hard", ref]);
|
|
715
|
+
const cleanResult = await this.cleanUntracked(dir);
|
|
716
|
+
return cleanResult;
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
async getMergeStatus(dir) {
|
|
720
|
+
const mergeHeadPath = path.join(dir, ".git", "MERGE_HEAD");
|
|
721
|
+
const isMerging = fs.existsSync(mergeHeadPath);
|
|
722
|
+
|
|
723
|
+
if (!isMerging) {
|
|
724
|
+
return { merging: false, message: "Nenhum merge em andamento" };
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
// Get conflicted files
|
|
728
|
+
try {
|
|
729
|
+
const diff = await this._exec(dir, ["diff", "--name-only", "--diff-filter=U"]);
|
|
730
|
+
const conflicts = diff.split("\n").filter(Boolean);
|
|
731
|
+
return {
|
|
732
|
+
merging: true,
|
|
733
|
+
conflicts,
|
|
734
|
+
message: conflicts.length > 0
|
|
735
|
+
? `Merge em andamento com ${conflicts.length} conflito(s)`
|
|
736
|
+
: "Merge em andamento sem conflitos"
|
|
737
|
+
};
|
|
738
|
+
} catch {
|
|
739
|
+
return { merging: true, conflicts: [], message: "Merge em andamento" };
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
async abortMerge(dir) {
|
|
744
|
+
await this._exec(dir, ["merge", "--abort"]);
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
async merge(dir, branch, options = {}) {
|
|
748
|
+
const { message, noCommit, squash } = options;
|
|
749
|
+
const args = ["merge"];
|
|
750
|
+
if (noCommit) args.push("--no-commit");
|
|
751
|
+
if (squash) args.push("--squash");
|
|
752
|
+
if (message) args.push("-m", message);
|
|
753
|
+
args.push(branch);
|
|
754
|
+
|
|
755
|
+
try {
|
|
756
|
+
const result = await this._exec(dir, args);
|
|
757
|
+
return { merged: true, message: result };
|
|
758
|
+
} catch (e) {
|
|
759
|
+
if (e.message.includes("conflict")) {
|
|
760
|
+
const diff = await this._exec(dir, ["diff", "--name-only", "--diff-filter=U"]);
|
|
761
|
+
const conflicts = diff.split("\n").filter(Boolean);
|
|
762
|
+
return { merged: false, conflicts, message: "Merge conflicts detected" };
|
|
763
|
+
}
|
|
764
|
+
if (e.message.includes("Already up to date")) {
|
|
765
|
+
return { merged: true, message: "Already up to date" };
|
|
766
|
+
}
|
|
767
|
+
throw e;
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
// ============ DIFF/CLONE ============
|
|
772
|
+
async diff(dir, options = {}) { return await this._exec(dir, ["diff"]); }
|
|
773
|
+
async diffCommits(dir, from, to) { return await this._exec(dir, ["diff", from, to]); }
|
|
774
|
+
async diffStats(dir, from, to) { const out = await this._exec(dir, ["diff", "--stat", from, to]); return { message: out }; }
|
|
775
|
+
|
|
776
|
+
async clone(url, dir, options = {}) {
|
|
777
|
+
const { branch, depth, singleBranch } = options;
|
|
778
|
+
const args = ["clone"];
|
|
779
|
+
if (branch) args.push("-b", branch);
|
|
780
|
+
if (depth) args.push("--depth", depth.toString());
|
|
781
|
+
if (singleBranch) args.push("--single-branch");
|
|
782
|
+
args.push(url, ".");
|
|
783
|
+
|
|
784
|
+
const header = this._getAuthHeader(url);
|
|
785
|
+
const cmdArgs = [];
|
|
786
|
+
if (header) cmdArgs.push("-c", `http.extraHeader=${header}`);
|
|
787
|
+
cmdArgs.push(...args);
|
|
788
|
+
|
|
789
|
+
await fs.promises.mkdir(dir, { recursive: true });
|
|
790
|
+
await this._exec(dir, cmdArgs);
|
|
791
|
+
return { branch: branch || "HEAD" };
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
async listGitignore(dir) {
|
|
795
|
+
if (fs.existsSync(path.join(dir, ".gitignore"))) {
|
|
796
|
+
return fs.readFileSync(path.join(dir, ".gitignore"), "utf8").split("\n");
|
|
797
|
+
}
|
|
798
|
+
return [];
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
async createGitignore(dir, patterns) {
|
|
802
|
+
fs.writeFileSync(path.join(dir, ".gitignore"), patterns.join("\n"));
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
async addToGitignore(dir, patterns) {
|
|
806
|
+
fs.appendFileSync(path.join(dir, ".gitignore"), "\n" + patterns.join("\n"));
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
async removeFromGitignore(dir, patterns) {
|
|
810
|
+
const p = path.join(dir, ".gitignore");
|
|
811
|
+
if (!fs.existsSync(p)) return;
|
|
812
|
+
let c = fs.readFileSync(p, "utf8");
|
|
813
|
+
patterns.forEach(pat => c = c.replace(pat, ""));
|
|
814
|
+
fs.writeFileSync(p, c);
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
async listRemotesRaw(dir) {
|
|
818
|
+
return this.listRemotes(dir);
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
async cleanUntracked(dir) {
|
|
822
|
+
// dry-run first to get list
|
|
823
|
+
const dry = await this._exec(dir, ["clean", "-n", "-d"]);
|
|
824
|
+
const files = dry.split("\n").filter(Boolean).map(l => l.replace("Would remove ", "").trim());
|
|
825
|
+
|
|
826
|
+
if (files.length > 0) {
|
|
827
|
+
await this._exec(dir, ["clean", "-f", "-d"]);
|
|
828
|
+
}
|
|
829
|
+
return { cleaned: files };
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
async listRemotes(dir) {
|
|
833
|
+
const out = await this._exec(dir, ["remote", "-v"]);
|
|
834
|
+
const map = new Map();
|
|
835
|
+
out.split("\n").filter(Boolean).forEach(line => {
|
|
836
|
+
const [name, rest] = line.split("\t");
|
|
837
|
+
const url = rest.split(" ")[0];
|
|
838
|
+
map.set(name, { remote: name, url });
|
|
839
|
+
});
|
|
840
|
+
return Array.from(map.values());
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
// ============ GIT LFS SUPPORT ============
|
|
844
|
+
|
|
845
|
+
/**
|
|
846
|
+
* Verifica se Git LFS está instalado
|
|
847
|
+
*/
|
|
848
|
+
async isLfsInstalled() {
|
|
849
|
+
try {
|
|
850
|
+
await this._exec(".", ["lfs", "version"]);
|
|
851
|
+
return true;
|
|
852
|
+
} catch {
|
|
853
|
+
return false;
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
/**
|
|
858
|
+
* Inicializa Git LFS no repositório
|
|
859
|
+
*/
|
|
860
|
+
async lfsInstall(dir) {
|
|
861
|
+
await this._exec(dir, ["lfs", "install"]);
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
/**
|
|
865
|
+
* Rastreia arquivos com Git LFS
|
|
866
|
+
* @param {string} dir - Diretório do repositório
|
|
867
|
+
* @param {string[]} patterns - Padrões de arquivo (ex: ["*.psd", "*.zip"])
|
|
868
|
+
*/
|
|
869
|
+
async lfsTrack(dir, patterns) {
|
|
870
|
+
for (const pattern of patterns) {
|
|
871
|
+
await this._exec(dir, ["lfs", "track", pattern]);
|
|
872
|
+
}
|
|
873
|
+
// Adiciona .gitattributes automaticamente
|
|
874
|
+
await this._exec(dir, ["add", ".gitattributes"]);
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
/**
|
|
878
|
+
* Remove rastreamento LFS de padrões
|
|
879
|
+
*/
|
|
880
|
+
async lfsUntrack(dir, patterns) {
|
|
881
|
+
for (const pattern of patterns) {
|
|
882
|
+
await this._exec(dir, ["lfs", "untrack", pattern]);
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
/**
|
|
887
|
+
* Lista arquivos rastreados por LFS
|
|
888
|
+
*/
|
|
889
|
+
async lfsList(dir) {
|
|
890
|
+
const out = await this._exec(dir, ["lfs", "ls-files"]);
|
|
891
|
+
return out.split("\n").filter(Boolean).map(line => {
|
|
892
|
+
const parts = line.split(" - ");
|
|
893
|
+
return {
|
|
894
|
+
oid: parts[0]?.trim(),
|
|
895
|
+
file: parts[1]?.trim()
|
|
896
|
+
};
|
|
897
|
+
});
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
/**
|
|
901
|
+
* Lista padrões rastreados por LFS
|
|
902
|
+
*/
|
|
903
|
+
async lfsTrackedPatterns(dir) {
|
|
904
|
+
const out = await this._exec(dir, ["lfs", "track"]);
|
|
905
|
+
return out.split("\n")
|
|
906
|
+
.filter(line => line.trim().startsWith("*") || line.includes("("))
|
|
907
|
+
.map(line => line.trim().split(" ")[0]);
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
/**
|
|
911
|
+
* Baixa arquivos LFS
|
|
912
|
+
*/
|
|
913
|
+
async lfsPull(dir) {
|
|
914
|
+
await this._exec(dir, ["lfs", "pull"]);
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
/**
|
|
918
|
+
* Envia arquivos LFS
|
|
919
|
+
*/
|
|
920
|
+
async lfsPush(dir, remote = "origin") {
|
|
921
|
+
await this._exec(dir, ["lfs", "push", "--all", remote]);
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
/**
|
|
925
|
+
* Status do Git LFS
|
|
926
|
+
*/
|
|
927
|
+
async lfsStatus(dir) {
|
|
928
|
+
const out = await this._exec(dir, ["lfs", "status"]);
|
|
929
|
+
return out;
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
|