phantomx-tool-client 1.0.8 → 1.1.0
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/dist/Services/Logger.js +1 -1
- package/dist/Services/Logger.js.map +1 -1
- package/dist/__tests__/toolExecutionService_tempAI.test.js +107 -0
- package/dist/__tests__/toolExecutionService_tempAI.test.js.map +1 -1
- package/dist/githubOperationsHanlder.d.ts +10 -58
- package/dist/githubOperationsHanlder.d.ts.map +1 -1
- package/dist/githubOperationsHanlder.js +337 -268
- package/dist/githubOperationsHanlder.js.map +1 -1
- package/dist/tool-server.d.ts +3 -0
- package/dist/tool-server.d.ts.map +1 -1
- package/dist/tool-server.js +189 -6
- package/dist/tool-server.js.map +1 -1
- package/dist/toolExecutionService.d.ts.map +1 -1
- package/dist/toolExecutionService.js +9 -12
- package/dist/toolExecutionService.js.map +1 -1
- package/package.json +61 -60
|
@@ -40,6 +40,8 @@ exports.getRepoList = getRepoList;
|
|
|
40
40
|
exports.getRepoBranch = getRepoBranch;
|
|
41
41
|
const Logger_1 = require("./Services/Logger");
|
|
42
42
|
const child_process_1 = require("child_process");
|
|
43
|
+
const fs = __importStar(require("fs"));
|
|
44
|
+
const path = __importStar(require("path"));
|
|
43
45
|
// ---------------------------------------------------------------------------
|
|
44
46
|
// Local bash executor — drop-in replacement for ssh.executeCommand()
|
|
45
47
|
// Returns { success, output, error, code } matching the SSH client response.
|
|
@@ -313,6 +315,246 @@ async function getSingleFileDiff(repoPath, filePath, status) {
|
|
|
313
315
|
}
|
|
314
316
|
}
|
|
315
317
|
class GithubOperationsService {
|
|
318
|
+
constructor(localWorkspacePath) {
|
|
319
|
+
this.localWorkspacePath = localWorkspacePath;
|
|
320
|
+
}
|
|
321
|
+
resolveWorkspacePath(targetPath) {
|
|
322
|
+
const workspace = path.resolve(this.localWorkspacePath || tool_server_1.folderPath);
|
|
323
|
+
const resolved = targetPath
|
|
324
|
+
? path.resolve(path.isAbsolute(targetPath) ? targetPath : path.join(workspace, targetPath))
|
|
325
|
+
: workspace;
|
|
326
|
+
const relative = path.relative(workspace, resolved);
|
|
327
|
+
if (relative.startsWith('..') || path.isAbsolute(relative)) {
|
|
328
|
+
throw new Error(`Path is outside the workspace: ${targetPath}`);
|
|
329
|
+
}
|
|
330
|
+
return resolved;
|
|
331
|
+
}
|
|
332
|
+
resolveLocalRepository(repoPathOrName) {
|
|
333
|
+
const candidate = path.isAbsolute(repoPathOrName)
|
|
334
|
+
? repoPathOrName
|
|
335
|
+
: path.join(this.localWorkspacePath || tool_server_1.folderPath, repoPathOrName);
|
|
336
|
+
const repoPath = this.resolveWorkspacePath(candidate);
|
|
337
|
+
if (!fs.existsSync(repoPath)) {
|
|
338
|
+
throw new Error(`Repository path does not exist: ${repoPath}`);
|
|
339
|
+
}
|
|
340
|
+
if (!fs.existsSync(path.join(repoPath, '.git'))) {
|
|
341
|
+
throw new Error(`Not a git repository: ${repoPath}`);
|
|
342
|
+
}
|
|
343
|
+
return repoPath;
|
|
344
|
+
}
|
|
345
|
+
async findLocalGitRepositories(rootPath) {
|
|
346
|
+
const repositories = [];
|
|
347
|
+
const search = async (currentPath, depth) => {
|
|
348
|
+
if (depth > 10)
|
|
349
|
+
return;
|
|
350
|
+
if (fs.existsSync(path.join(currentPath, '.git'))) {
|
|
351
|
+
repositories.push(currentPath);
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
let entries;
|
|
355
|
+
try {
|
|
356
|
+
entries = await fs.promises.readdir(currentPath, { withFileTypes: true });
|
|
357
|
+
}
|
|
358
|
+
catch {
|
|
359
|
+
logger.warn(`Cannot read directory: ${currentPath}`);
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
for (const entry of entries) {
|
|
363
|
+
if (!entry.isDirectory() || entry.name.startsWith('.') || entry.name === 'node_modules')
|
|
364
|
+
continue;
|
|
365
|
+
await search(path.join(currentPath, entry.name), depth + 1);
|
|
366
|
+
}
|
|
367
|
+
};
|
|
368
|
+
await search(rootPath, 0);
|
|
369
|
+
return repositories;
|
|
370
|
+
}
|
|
371
|
+
getDiffStats(fileStats) {
|
|
372
|
+
return {
|
|
373
|
+
insertions: Number(fileStats === null || fileStats === void 0 ? void 0 : fileStats.insertions) || 0,
|
|
374
|
+
deletions: Number(fileStats === null || fileStats === void 0 ? void 0 : fileStats.deletions) || 0
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
async getLocalRepositoryChanges(repoPath) {
|
|
378
|
+
const git = (0, simple_git_1.simpleGit)(repoPath);
|
|
379
|
+
const status = await git.status();
|
|
380
|
+
const stagedSummary = await git.diffSummary(['--cached']);
|
|
381
|
+
const unstagedSummary = await git.diffSummary();
|
|
382
|
+
const changes = [];
|
|
383
|
+
const processed = new Set();
|
|
384
|
+
const addChange = (file, statusCode, insertions, deletions) => {
|
|
385
|
+
changes.push({ file, status: statusCode, insertions, deletions, changes: insertions + deletions });
|
|
386
|
+
processed.add(file);
|
|
387
|
+
};
|
|
388
|
+
for (const file of status.modified) {
|
|
389
|
+
const isStaged = status.staged.includes(file);
|
|
390
|
+
const unstaged = this.getDiffStats(unstagedSummary.files.find(item => item.file === file));
|
|
391
|
+
const staged = isStaged
|
|
392
|
+
? this.getDiffStats(stagedSummary.files.find(item => item.file === file))
|
|
393
|
+
: { insertions: 0, deletions: 0 };
|
|
394
|
+
addChange(file, isStaged ? 'MM' : 'M', unstaged.insertions + staged.insertions, unstaged.deletions + staged.deletions);
|
|
395
|
+
}
|
|
396
|
+
for (const file of status.staged) {
|
|
397
|
+
if (processed.has(file) || status.deleted.includes(file))
|
|
398
|
+
continue;
|
|
399
|
+
const stats = this.getDiffStats(stagedSummary.files.find(item => item.file === file));
|
|
400
|
+
addChange(file, 'A', stats.insertions, stats.deletions);
|
|
401
|
+
}
|
|
402
|
+
for (const file of status.not_added) {
|
|
403
|
+
if (processed.has(file))
|
|
404
|
+
continue;
|
|
405
|
+
try {
|
|
406
|
+
const stat = await fs.promises.stat(path.join(repoPath, file));
|
|
407
|
+
addChange(file, '?', Math.max(1, Math.ceil(stat.size / 50)), 0);
|
|
408
|
+
}
|
|
409
|
+
catch {
|
|
410
|
+
addChange(file, '?', 1, 0);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
for (const file of status.created || []) {
|
|
414
|
+
if (processed.has(file))
|
|
415
|
+
continue;
|
|
416
|
+
try {
|
|
417
|
+
const stat = await fs.promises.stat(path.join(repoPath, file));
|
|
418
|
+
addChange(file, '?', Math.max(1, Math.ceil(stat.size / 50)), 0);
|
|
419
|
+
}
|
|
420
|
+
catch {
|
|
421
|
+
addChange(file, '?', 1, 0);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
for (const file of status.deleted) {
|
|
425
|
+
if (processed.has(file))
|
|
426
|
+
continue;
|
|
427
|
+
const summary = status.staged.includes(file) ? stagedSummary : unstagedSummary;
|
|
428
|
+
const stats = this.getDiffStats(summary.files.find(item => item.file === file));
|
|
429
|
+
addChange(file, 'D', 0, stats.deletions || 1);
|
|
430
|
+
}
|
|
431
|
+
for (const rename of status.renamed) {
|
|
432
|
+
if (processed.has(rename.to))
|
|
433
|
+
continue;
|
|
434
|
+
const staged = this.getDiffStats(stagedSummary.files.find(item => item.file === rename.to));
|
|
435
|
+
const unstaged = this.getDiffStats(unstagedSummary.files.find(item => item.file === rename.to));
|
|
436
|
+
addChange(rename.to, 'R', staged.insertions + unstaged.insertions, staged.deletions + unstaged.deletions);
|
|
437
|
+
}
|
|
438
|
+
const totalInsertions = changes.reduce((total, change) => total + change.insertions, 0);
|
|
439
|
+
const totalDeletions = changes.reduce((total, change) => total + change.deletions, 0);
|
|
440
|
+
return {
|
|
441
|
+
repoName: path.basename(repoPath),
|
|
442
|
+
repoPath,
|
|
443
|
+
changes,
|
|
444
|
+
summary: {
|
|
445
|
+
totalFiles: changes.length,
|
|
446
|
+
totalInsertions,
|
|
447
|
+
totalDeletions,
|
|
448
|
+
totalChanges: totalInsertions + totalDeletions
|
|
449
|
+
}
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
async getGitChanges(requestedPath) {
|
|
453
|
+
try {
|
|
454
|
+
const rootPath = this.resolveWorkspacePath(requestedPath);
|
|
455
|
+
if (!fs.existsSync(rootPath)) {
|
|
456
|
+
return { success: false, error: `Path does not exist: ${rootPath}` };
|
|
457
|
+
}
|
|
458
|
+
const repoPaths = await this.findLocalGitRepositories(rootPath);
|
|
459
|
+
if (repoPaths.length === 0) {
|
|
460
|
+
return { success: false, error: `No git repositories found in: ${rootPath}` };
|
|
461
|
+
}
|
|
462
|
+
const repositories = [];
|
|
463
|
+
for (const repoPath of repoPaths) {
|
|
464
|
+
try {
|
|
465
|
+
repositories.push(await this.getLocalRepositoryChanges(repoPath));
|
|
466
|
+
}
|
|
467
|
+
catch (error) {
|
|
468
|
+
logger.error(`Failed to inspect repository ${repoPath}`, error);
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
const totalFiles = repositories.reduce((total, repo) => total + repo.summary.totalFiles, 0);
|
|
472
|
+
const totalInsertions = repositories.reduce((total, repo) => total + repo.summary.totalInsertions, 0);
|
|
473
|
+
const totalDeletions = repositories.reduce((total, repo) => total + repo.summary.totalDeletions, 0);
|
|
474
|
+
return {
|
|
475
|
+
success: true,
|
|
476
|
+
repositories,
|
|
477
|
+
overallSummary: {
|
|
478
|
+
totalRepositories: repositories.length,
|
|
479
|
+
totalFiles,
|
|
480
|
+
totalInsertions,
|
|
481
|
+
totalDeletions,
|
|
482
|
+
totalChanges: totalInsertions + totalDeletions
|
|
483
|
+
}
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
catch (error) {
|
|
487
|
+
return { success: false, error: error instanceof Error ? error.message : String(error) };
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
async getFileDiff(repoPathOrName, filePath, statusCode) {
|
|
491
|
+
const repoPath = this.resolveLocalRepository(repoPathOrName);
|
|
492
|
+
const fullFilePath = this.resolveWorkspacePath(path.join(repoPath, filePath));
|
|
493
|
+
const relativeFilePath = path.relative(repoPath, fullFilePath);
|
|
494
|
+
if (relativeFilePath.startsWith('..') || path.isAbsolute(relativeFilePath)) {
|
|
495
|
+
throw new Error(`File is outside the repository: ${filePath}`);
|
|
496
|
+
}
|
|
497
|
+
const git = (0, simple_git_1.simpleGit)(repoPath);
|
|
498
|
+
let diff = '';
|
|
499
|
+
if (statusCode === 'MM') {
|
|
500
|
+
const staged = await git.diff(['--cached', relativeFilePath]);
|
|
501
|
+
const unstaged = await git.diff([relativeFilePath]);
|
|
502
|
+
diff = `--- STAGED CHANGES ---\n${staged}\n\n--- UNSTAGED CHANGES ---\n${unstaged}`;
|
|
503
|
+
}
|
|
504
|
+
else if (statusCode === 'A') {
|
|
505
|
+
diff = await git.diff(['--cached', relativeFilePath]);
|
|
506
|
+
}
|
|
507
|
+
else if (statusCode === '?') {
|
|
508
|
+
if (fs.existsSync(fullFilePath)) {
|
|
509
|
+
const lines = (await fs.promises.readFile(fullFilePath, 'utf8')).split('\n');
|
|
510
|
+
diff = `+++ b/${relativeFilePath}\n@@ -0,0 +1,${lines.length} @@\n${lines.map(line => `+${line}`).join('\n')}\n`;
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
else if (statusCode === 'D') {
|
|
514
|
+
const currentStatus = await git.status();
|
|
515
|
+
const args = currentStatus.staged.includes(relativeFilePath)
|
|
516
|
+
? ['--cached', 'HEAD', relativeFilePath]
|
|
517
|
+
: ['HEAD', relativeFilePath];
|
|
518
|
+
diff = await git.diff(args).catch(() => '');
|
|
519
|
+
}
|
|
520
|
+
else {
|
|
521
|
+
diff = await git.diff([relativeFilePath]);
|
|
522
|
+
}
|
|
523
|
+
return { success: true, diff, filePath: relativeFilePath, repoPath };
|
|
524
|
+
}
|
|
525
|
+
async discardFileChanges(repoPathOrName, filePath) {
|
|
526
|
+
const repoPath = this.resolveLocalRepository(repoPathOrName);
|
|
527
|
+
const fullFilePath = this.resolveWorkspacePath(path.join(repoPath, filePath));
|
|
528
|
+
const relativeFilePath = path.relative(repoPath, fullFilePath);
|
|
529
|
+
if (relativeFilePath.startsWith('..') || path.isAbsolute(relativeFilePath)) {
|
|
530
|
+
throw new Error(`File is outside the repository: ${filePath}`);
|
|
531
|
+
}
|
|
532
|
+
const git = (0, simple_git_1.simpleGit)(repoPath);
|
|
533
|
+
const status = await git.status();
|
|
534
|
+
const isUntracked = status.not_added.includes(relativeFilePath) || (status.created || []).includes(relativeFilePath);
|
|
535
|
+
if (isUntracked) {
|
|
536
|
+
if (!fs.existsSync(fullFilePath)) {
|
|
537
|
+
return { success: false, error: `File '${relativeFilePath}' does not exist.` };
|
|
538
|
+
}
|
|
539
|
+
const stat = await fs.promises.lstat(fullFilePath);
|
|
540
|
+
if (!stat.isFile()) {
|
|
541
|
+
return { success: false, error: `Only untracked files can be discarded: ${relativeFilePath}` };
|
|
542
|
+
}
|
|
543
|
+
await fs.promises.unlink(fullFilePath);
|
|
544
|
+
return { success: true, message: `Untracked file '${relativeFilePath}' has been removed.` };
|
|
545
|
+
}
|
|
546
|
+
const hasChanges = status.modified.includes(relativeFilePath)
|
|
547
|
+
|| status.staged.includes(relativeFilePath)
|
|
548
|
+
|| status.deleted.includes(relativeFilePath);
|
|
549
|
+
if (!hasChanges) {
|
|
550
|
+
return { success: false, error: `File '${relativeFilePath}' has no changes to discard.` };
|
|
551
|
+
}
|
|
552
|
+
if (status.staged.includes(relativeFilePath)) {
|
|
553
|
+
await git.reset(['HEAD', relativeFilePath]);
|
|
554
|
+
}
|
|
555
|
+
await git.checkout(['HEAD', '--', relativeFilePath]);
|
|
556
|
+
return { success: true, message: `Changes to '${relativeFilePath}' have been discarded.` };
|
|
557
|
+
}
|
|
316
558
|
escapeSingleQuotedShell(value) {
|
|
317
559
|
return value.replace(/'/g, `'\\''`);
|
|
318
560
|
}
|
|
@@ -439,21 +681,21 @@ class GithubOperationsService {
|
|
|
439
681
|
try {
|
|
440
682
|
logger.info('Starting git push operation', { repoName });
|
|
441
683
|
// Combine all commands into a single SSH call
|
|
442
|
-
const combinedCommand = `
|
|
443
|
-
cd ${repoPath} &&
|
|
444
|
-
BRANCH=$(sudo git rev-parse --abbrev-ref HEAD) &&
|
|
445
|
-
COMMITS_AHEAD=$(sudo git rev-list --count origin/$BRANCH..HEAD 2>/dev/null || echo 0) &&
|
|
446
|
-
if [ "$COMMITS_AHEAD" = "0" ]; then
|
|
447
|
-
echo "ERROR:NO_COMMITS";
|
|
448
|
-
exit 1;
|
|
449
|
-
fi &&
|
|
450
|
-
sudo git remote set-url origin https://x-access-token:${installationToken}@github.com/${gitOrgName}/${repoName}.git &&
|
|
451
|
-
sudo git push origin $BRANCH &&
|
|
452
|
-
sudo git remote set-url origin https://github.com/${gitOrgName}/${repoName}.git &&
|
|
453
|
-
COMMIT_HASH=$(sudo git rev-parse HEAD) &&
|
|
454
|
-
echo "BRANCH:$BRANCH" &&
|
|
455
|
-
echo "COMMITS_AHEAD:$COMMITS_AHEAD" &&
|
|
456
|
-
echo "COMMIT_HASH:$COMMIT_HASH"
|
|
684
|
+
const combinedCommand = `
|
|
685
|
+
cd ${repoPath} &&
|
|
686
|
+
BRANCH=$(sudo git rev-parse --abbrev-ref HEAD) &&
|
|
687
|
+
COMMITS_AHEAD=$(sudo git rev-list --count origin/$BRANCH..HEAD 2>/dev/null || echo 0) &&
|
|
688
|
+
if [ "$COMMITS_AHEAD" = "0" ]; then
|
|
689
|
+
echo "ERROR:NO_COMMITS";
|
|
690
|
+
exit 1;
|
|
691
|
+
fi &&
|
|
692
|
+
sudo git remote set-url origin https://x-access-token:${installationToken}@github.com/${gitOrgName}/${repoName}.git &&
|
|
693
|
+
sudo git push origin $BRANCH &&
|
|
694
|
+
sudo git remote set-url origin https://github.com/${gitOrgName}/${repoName}.git &&
|
|
695
|
+
COMMIT_HASH=$(sudo git rev-parse HEAD) &&
|
|
696
|
+
echo "BRANCH:$BRANCH" &&
|
|
697
|
+
echo "COMMITS_AHEAD:$COMMITS_AHEAD" &&
|
|
698
|
+
echo "COMMIT_HASH:$COMMIT_HASH"
|
|
457
699
|
`.replace(/\n\s+/g, ' ');
|
|
458
700
|
const result = await executeShell(combinedCommand);
|
|
459
701
|
if (!result.success) {
|
|
@@ -496,23 +738,23 @@ class GithubOperationsService {
|
|
|
496
738
|
try {
|
|
497
739
|
logger.info('Checking repository status', { repoName });
|
|
498
740
|
// Combine all git commands into a single SSH call
|
|
499
|
-
const combinedCommand = `
|
|
500
|
-
cd ${repoPath} &&
|
|
501
|
-
BRANCH=$(sudo git rev-parse --abbrev-ref HEAD) &&
|
|
502
|
-
sudo git remote set-url origin https://x-access-token:${installationToken}@github.com/${gitOrgName}/${repoName}.git &&
|
|
503
|
-
sudo git fetch origin &&
|
|
504
|
-
sudo git remote set-url origin https://github.com/${gitOrgName}/${repoName}.git &&
|
|
505
|
-
BEHIND=$(sudo git rev-list --count HEAD..origin/$BRANCH 2>/dev/null || echo 0) &&
|
|
506
|
-
AHEAD=$(sudo git rev-list --count origin/$BRANCH..HEAD 2>/dev/null || echo 0) &&
|
|
507
|
-
STATUS=$(sudo git status --porcelain) &&
|
|
508
|
-
CURRENT_HASH=$(sudo git rev-parse HEAD) &&
|
|
509
|
-
REMOTE_HASH=$(sudo git rev-parse origin/$BRANCH 2>/dev/null || echo unknown) &&
|
|
510
|
-
echo "BRANCH:$BRANCH" &&
|
|
511
|
-
echo "BEHIND:$BEHIND" &&
|
|
512
|
-
echo "AHEAD:$AHEAD" &&
|
|
513
|
-
echo "STATUS:$STATUS" &&
|
|
514
|
-
echo "CURRENT:$CURRENT_HASH" &&
|
|
515
|
-
echo "REMOTE:$REMOTE_HASH"
|
|
741
|
+
const combinedCommand = `
|
|
742
|
+
cd ${repoPath} &&
|
|
743
|
+
BRANCH=$(sudo git rev-parse --abbrev-ref HEAD) &&
|
|
744
|
+
sudo git remote set-url origin https://x-access-token:${installationToken}@github.com/${gitOrgName}/${repoName}.git &&
|
|
745
|
+
sudo git fetch origin &&
|
|
746
|
+
sudo git remote set-url origin https://github.com/${gitOrgName}/${repoName}.git &&
|
|
747
|
+
BEHIND=$(sudo git rev-list --count HEAD..origin/$BRANCH 2>/dev/null || echo 0) &&
|
|
748
|
+
AHEAD=$(sudo git rev-list --count origin/$BRANCH..HEAD 2>/dev/null || echo 0) &&
|
|
749
|
+
STATUS=$(sudo git status --porcelain) &&
|
|
750
|
+
CURRENT_HASH=$(sudo git rev-parse HEAD) &&
|
|
751
|
+
REMOTE_HASH=$(sudo git rev-parse origin/$BRANCH 2>/dev/null || echo unknown) &&
|
|
752
|
+
echo "BRANCH:$BRANCH" &&
|
|
753
|
+
echo "BEHIND:$BEHIND" &&
|
|
754
|
+
echo "AHEAD:$AHEAD" &&
|
|
755
|
+
echo "STATUS:$STATUS" &&
|
|
756
|
+
echo "CURRENT:$CURRENT_HASH" &&
|
|
757
|
+
echo "REMOTE:$REMOTE_HASH"
|
|
516
758
|
`.replace(/\n\s+/g, ' ');
|
|
517
759
|
const result = await executeShell(combinedCommand);
|
|
518
760
|
if (!result.success) {
|
|
@@ -709,23 +951,23 @@ class GithubOperationsService {
|
|
|
709
951
|
const escapedAuthorName = commitAuthorName.replace(/"/g, '\\"');
|
|
710
952
|
const escapedAuthorEmail = commitAuthorEmail.replace(/"/g, '\\"');
|
|
711
953
|
// Combine all commands into a single SSH call
|
|
712
|
-
const combinedCommand = `
|
|
713
|
-
cd ${repoPath} &&
|
|
714
|
-
sudo git config user.name "${escapedAuthorName}" &&
|
|
715
|
-
sudo git config user.email "${escapedAuthorEmail}" &&
|
|
716
|
-
STATUS=$(sudo git status --porcelain) &&
|
|
717
|
-
if [ -z "$STATUS" ]; then
|
|
718
|
-
echo "ERROR:NO_CHANGES";
|
|
719
|
-
exit 1;
|
|
720
|
-
fi &&
|
|
721
|
-
CHANGED_FILES=$(echo "$STATUS" | wc -l) &&
|
|
722
|
-
sudo git add . &&
|
|
723
|
-
sudo git commit -m "${escapedCommitMsg}" &&
|
|
724
|
-
COMMIT_HASH=$(sudo git rev-parse HEAD) &&
|
|
725
|
-
SHORT_HASH=$(sudo git rev-parse --short HEAD) &&
|
|
726
|
-
echo "CHANGED_FILES:$CHANGED_FILES" &&
|
|
727
|
-
echo "COMMIT_HASH:$COMMIT_HASH" &&
|
|
728
|
-
echo "SHORT_HASH:$SHORT_HASH"
|
|
954
|
+
const combinedCommand = `
|
|
955
|
+
cd ${repoPath} &&
|
|
956
|
+
sudo git config user.name "${escapedAuthorName}" &&
|
|
957
|
+
sudo git config user.email "${escapedAuthorEmail}" &&
|
|
958
|
+
STATUS=$(sudo git status --porcelain) &&
|
|
959
|
+
if [ -z "$STATUS" ]; then
|
|
960
|
+
echo "ERROR:NO_CHANGES";
|
|
961
|
+
exit 1;
|
|
962
|
+
fi &&
|
|
963
|
+
CHANGED_FILES=$(echo "$STATUS" | wc -l) &&
|
|
964
|
+
sudo git add . &&
|
|
965
|
+
sudo git commit -m "${escapedCommitMsg}" &&
|
|
966
|
+
COMMIT_HASH=$(sudo git rev-parse HEAD) &&
|
|
967
|
+
SHORT_HASH=$(sudo git rev-parse --short HEAD) &&
|
|
968
|
+
echo "CHANGED_FILES:$CHANGED_FILES" &&
|
|
969
|
+
echo "COMMIT_HASH:$COMMIT_HASH" &&
|
|
970
|
+
echo "SHORT_HASH:$SHORT_HASH"
|
|
729
971
|
`.replace(/\n\s+/g, ' ');
|
|
730
972
|
const result = await executeShell(combinedCommand);
|
|
731
973
|
if (!result.success) {
|
|
@@ -810,20 +1052,20 @@ class GithubOperationsService {
|
|
|
810
1052
|
try {
|
|
811
1053
|
logger.info('Creating pull request', { repoName, targetBranch });
|
|
812
1054
|
// Combine commands into a single SSH call, with remote URL set/unset
|
|
813
|
-
const combinedCommand = `
|
|
814
|
-
cd ${repoPath} &&
|
|
815
|
-
sudo git remote set-url origin https://x-access-token:${installationToken}@github.com/${gitOrgName}/${repoName}.git &&
|
|
816
|
-
HEAD_BRANCH=$(sudo git rev-parse --abbrev-ref HEAD) &&
|
|
817
|
-
sudo git fetch origin ${targetBranch} 2>/dev/null &&
|
|
818
|
-
COMMITS_AHEAD=$(sudo git rev-list --count origin/${targetBranch}..$HEAD_BRANCH 2>/dev/null || echo 0) &&
|
|
819
|
-
if [ "$COMMITS_AHEAD" = "0" ]; then
|
|
820
|
-
echo "ERROR:NO_COMMITS:$HEAD_BRANCH";
|
|
821
|
-
sudo git remote set-url origin https://github.com/${gitOrgName}/${repoName}.git;
|
|
822
|
-
exit 1;
|
|
823
|
-
fi &&
|
|
824
|
-
echo "HEAD_BRANCH:$HEAD_BRANCH" &&
|
|
825
|
-
echo "COMMITS_AHEAD:$COMMITS_AHEAD" &&
|
|
826
|
-
sudo git remote set-url origin https://github.com/${gitOrgName}/${repoName}.git
|
|
1055
|
+
const combinedCommand = `
|
|
1056
|
+
cd ${repoPath} &&
|
|
1057
|
+
sudo git remote set-url origin https://x-access-token:${installationToken}@github.com/${gitOrgName}/${repoName}.git &&
|
|
1058
|
+
HEAD_BRANCH=$(sudo git rev-parse --abbrev-ref HEAD) &&
|
|
1059
|
+
sudo git fetch origin ${targetBranch} 2>/dev/null &&
|
|
1060
|
+
COMMITS_AHEAD=$(sudo git rev-list --count origin/${targetBranch}..$HEAD_BRANCH 2>/dev/null || echo 0) &&
|
|
1061
|
+
if [ "$COMMITS_AHEAD" = "0" ]; then
|
|
1062
|
+
echo "ERROR:NO_COMMITS:$HEAD_BRANCH";
|
|
1063
|
+
sudo git remote set-url origin https://github.com/${gitOrgName}/${repoName}.git;
|
|
1064
|
+
exit 1;
|
|
1065
|
+
fi &&
|
|
1066
|
+
echo "HEAD_BRANCH:$HEAD_BRANCH" &&
|
|
1067
|
+
echo "COMMITS_AHEAD:$COMMITS_AHEAD" &&
|
|
1068
|
+
sudo git remote set-url origin https://github.com/${gitOrgName}/${repoName}.git
|
|
827
1069
|
`.replace(/\n\s+/g, ' ');
|
|
828
1070
|
let result = await executeShell(combinedCommand);
|
|
829
1071
|
const output = result.output || '';
|
|
@@ -1020,20 +1262,20 @@ class GithubOperationsService {
|
|
|
1020
1262
|
const targetLocalBranch = resolvedTarget.displayBranch;
|
|
1021
1263
|
const safeTargetLocalBranch = this.escapeSingleQuotedShell(targetLocalBranch);
|
|
1022
1264
|
const safeSourceRef = this.escapeSingleQuotedShell(resolvedSource.resolvedRef);
|
|
1023
|
-
const prepareBranchCommand = `
|
|
1024
|
-
cd ${repoPath} &&
|
|
1025
|
-
TARGET_LOCAL='${safeTargetLocalBranch}' &&
|
|
1026
|
-
if sudo git show-ref --verify --quiet "refs/heads/$TARGET_LOCAL"; then
|
|
1027
|
-
sudo git checkout "$TARGET_LOCAL";
|
|
1028
|
-
elif sudo git show-ref --verify --quiet "refs/remotes/origin/$TARGET_LOCAL"; then
|
|
1029
|
-
sudo git checkout -b "$TARGET_LOCAL" "origin/$TARGET_LOCAL";
|
|
1030
|
-
else
|
|
1031
|
-
echo "ERROR:TARGET_BRANCH_NOT_CHECKOUTABLE";
|
|
1032
|
-
exit 1;
|
|
1033
|
-
fi &&
|
|
1034
|
-
if sudo git show-ref --verify --quiet "refs/remotes/origin/$TARGET_LOCAL"; then
|
|
1035
|
-
sudo git pull --ff-only origin "$TARGET_LOCAL" 2>/dev/null || true;
|
|
1036
|
-
fi
|
|
1265
|
+
const prepareBranchCommand = `
|
|
1266
|
+
cd ${repoPath} &&
|
|
1267
|
+
TARGET_LOCAL='${safeTargetLocalBranch}' &&
|
|
1268
|
+
if sudo git show-ref --verify --quiet "refs/heads/$TARGET_LOCAL"; then
|
|
1269
|
+
sudo git checkout "$TARGET_LOCAL";
|
|
1270
|
+
elif sudo git show-ref --verify --quiet "refs/remotes/origin/$TARGET_LOCAL"; then
|
|
1271
|
+
sudo git checkout -b "$TARGET_LOCAL" "origin/$TARGET_LOCAL";
|
|
1272
|
+
else
|
|
1273
|
+
echo "ERROR:TARGET_BRANCH_NOT_CHECKOUTABLE";
|
|
1274
|
+
exit 1;
|
|
1275
|
+
fi &&
|
|
1276
|
+
if sudo git show-ref --verify --quiet "refs/remotes/origin/$TARGET_LOCAL"; then
|
|
1277
|
+
sudo git pull --ff-only origin "$TARGET_LOCAL" 2>/dev/null || true;
|
|
1278
|
+
fi
|
|
1037
1279
|
`.replace(/\n\s+/g, ' ');
|
|
1038
1280
|
result = await executeShell(prepareBranchCommand);
|
|
1039
1281
|
if (!result.success) {
|
|
@@ -1131,16 +1373,16 @@ class GithubOperationsService {
|
|
|
1131
1373
|
try {
|
|
1132
1374
|
logger.info('Checking if PR exists', { repoName, targetBranch });
|
|
1133
1375
|
// Get current branch and commits ahead
|
|
1134
|
-
const combinedCommand = `
|
|
1135
|
-
cd ${repoPath} &&
|
|
1136
|
-
sudo git remote set-url origin https://x-access-token:${installationToken}@github.com/${gitOrgName}/${repoName}.git &&
|
|
1137
|
-
HEAD_BRANCH=$(sudo git rev-parse --abbrev-ref HEAD) &&
|
|
1138
|
-
sudo git fetch origin --prune 2>/dev/null || true &&
|
|
1139
|
-
sudo git fetch origin ${targetBranch} 2>/dev/null || true &&
|
|
1140
|
-
COMMITS_AHEAD=$(sudo git rev-list --count origin/${targetBranch}..$HEAD_BRANCH 2>/dev/null || echo 0) &&
|
|
1141
|
-
sudo git remote set-url origin https://github.com/${gitOrgName}/${repoName}.git &&
|
|
1142
|
-
echo "HEAD_BRANCH:$HEAD_BRANCH" &&
|
|
1143
|
-
echo "COMMITS_AHEAD:$COMMITS_AHEAD"
|
|
1376
|
+
const combinedCommand = `
|
|
1377
|
+
cd ${repoPath} &&
|
|
1378
|
+
sudo git remote set-url origin https://x-access-token:${installationToken}@github.com/${gitOrgName}/${repoName}.git &&
|
|
1379
|
+
HEAD_BRANCH=$(sudo git rev-parse --abbrev-ref HEAD) &&
|
|
1380
|
+
sudo git fetch origin --prune 2>/dev/null || true &&
|
|
1381
|
+
sudo git fetch origin ${targetBranch} 2>/dev/null || true &&
|
|
1382
|
+
COMMITS_AHEAD=$(sudo git rev-list --count origin/${targetBranch}..$HEAD_BRANCH 2>/dev/null || echo 0) &&
|
|
1383
|
+
sudo git remote set-url origin https://github.com/${gitOrgName}/${repoName}.git &&
|
|
1384
|
+
echo "HEAD_BRANCH:$HEAD_BRANCH" &&
|
|
1385
|
+
echo "COMMITS_AHEAD:$COMMITS_AHEAD"
|
|
1144
1386
|
`.replace(/\n\s+/g, ' ');
|
|
1145
1387
|
const result = await executeShell(combinedCommand);
|
|
1146
1388
|
// Check for command execution failure
|
|
@@ -1415,14 +1657,14 @@ class GithubOperationsService {
|
|
|
1415
1657
|
try {
|
|
1416
1658
|
logger.info('Fetching commit details', { repoName, commitHash });
|
|
1417
1659
|
// Get commit information and file changes in a single SSH call
|
|
1418
|
-
const combinedCommand = `
|
|
1419
|
-
cd ${repoPath} &&
|
|
1420
|
-
COMMIT_INFO=$(sudo git show --no-patch --pretty=format:"%H|%h|%an|%ae|%ad|%s" --date=iso ${commitHash}) &&
|
|
1421
|
-
FILE_STATS=$(sudo git show --stat --pretty="" --numstat ${commitHash}) &&
|
|
1422
|
-
echo "COMMIT_INFO:$COMMIT_INFO" &&
|
|
1423
|
-
echo "FILE_STATS_START" &&
|
|
1424
|
-
echo "$FILE_STATS" &&
|
|
1425
|
-
echo "FILE_STATS_END"
|
|
1660
|
+
const combinedCommand = `
|
|
1661
|
+
cd ${repoPath} &&
|
|
1662
|
+
COMMIT_INFO=$(sudo git show --no-patch --pretty=format:"%H|%h|%an|%ae|%ad|%s" --date=iso ${commitHash}) &&
|
|
1663
|
+
FILE_STATS=$(sudo git show --stat --pretty="" --numstat ${commitHash}) &&
|
|
1664
|
+
echo "COMMIT_INFO:$COMMIT_INFO" &&
|
|
1665
|
+
echo "FILE_STATS_START" &&
|
|
1666
|
+
echo "$FILE_STATS" &&
|
|
1667
|
+
echo "FILE_STATS_END"
|
|
1426
1668
|
`.replace(/\n\s+/g, ' ');
|
|
1427
1669
|
const result = await executeShell(combinedCommand);
|
|
1428
1670
|
if (!result.success) {
|
|
@@ -1716,179 +1958,6 @@ class GithubOperationsService {
|
|
|
1716
1958
|
});
|
|
1717
1959
|
});
|
|
1718
1960
|
}
|
|
1719
|
-
// ── New git operations ─────────────────────────────────────────────────────
|
|
1720
|
-
/**
|
|
1721
|
-
* Get all uncommitted git changes (metadata only, no diffs) for a repository.
|
|
1722
|
-
* Accepts repoName (looked up under folderPath) or a full absolute repoPath.
|
|
1723
|
-
*/
|
|
1724
|
-
async getGitChanges(repoName) {
|
|
1725
|
-
const pathModule = await Promise.resolve().then(() => __importStar(require('path')));
|
|
1726
|
-
const fs = await Promise.resolve().then(() => __importStar(require('fs')));
|
|
1727
|
-
logger.info(`[getGitChanges] repoName: ${repoName}`);
|
|
1728
|
-
try {
|
|
1729
|
-
// Resolve path: absolute as-is, otherwise join with folderPath
|
|
1730
|
-
const repoPath = pathModule.isAbsolute(repoName)
|
|
1731
|
-
? repoName
|
|
1732
|
-
: pathModule.join(tool_server_1.folderPath, repoName);
|
|
1733
|
-
if (!fs.existsSync(repoPath)) {
|
|
1734
|
-
return { success: false, error: `Path does not exist: ${repoPath}` };
|
|
1735
|
-
}
|
|
1736
|
-
// Case 1: the path itself is a git repository
|
|
1737
|
-
const gitDir = pathModule.join(repoPath, '.git');
|
|
1738
|
-
if (fs.existsSync(gitDir)) {
|
|
1739
|
-
logger.info(`[getGitChanges] Single repository detected: ${repoPath}`);
|
|
1740
|
-
const repoData = await getRepositoryChangesMetadataOnly(repoPath);
|
|
1741
|
-
return {
|
|
1742
|
-
success: true,
|
|
1743
|
-
repositories: [repoData],
|
|
1744
|
-
overallSummary: {
|
|
1745
|
-
totalRepositories: 1,
|
|
1746
|
-
totalFiles: repoData.summary.totalFiles,
|
|
1747
|
-
totalInsertions: repoData.summary.totalInsertions,
|
|
1748
|
-
totalDeletions: repoData.summary.totalDeletions,
|
|
1749
|
-
totalChanges: repoData.summary.totalChanges
|
|
1750
|
-
}
|
|
1751
|
-
};
|
|
1752
|
-
}
|
|
1753
|
-
// Case 2: not a git repo directly — search recursively for git repos inside
|
|
1754
|
-
logger.info(`[getGitChanges] Not a git repo, searching recursively in: ${repoPath}`);
|
|
1755
|
-
const gitRepositories = await findGitRepositories(repoPath);
|
|
1756
|
-
if (gitRepositories.length === 0) {
|
|
1757
|
-
return { success: false, error: `No git repositories found in: ${repoPath}` };
|
|
1758
|
-
}
|
|
1759
|
-
logger.info(`[getGitChanges] Found ${gitRepositories.length} git repositories`);
|
|
1760
|
-
const repositories = [];
|
|
1761
|
-
let overallFiles = 0, overallInsertions = 0, overallDeletions = 0, overallChanges = 0;
|
|
1762
|
-
for (const gitRepoPath of gitRepositories) {
|
|
1763
|
-
try {
|
|
1764
|
-
const repoData = await getRepositoryChangesMetadataOnly(gitRepoPath);
|
|
1765
|
-
repositories.push(repoData);
|
|
1766
|
-
overallFiles += repoData.summary.totalFiles;
|
|
1767
|
-
overallInsertions += repoData.summary.totalInsertions;
|
|
1768
|
-
overallDeletions += repoData.summary.totalDeletions;
|
|
1769
|
-
overallChanges += repoData.summary.totalChanges;
|
|
1770
|
-
logger.info(`[getGitChanges] ${repoData.repoName}: ${repoData.summary.totalFiles} files changed`);
|
|
1771
|
-
}
|
|
1772
|
-
catch (repoError) {
|
|
1773
|
-
logger.error(`[getGitChanges] Error processing repo ${gitRepoPath}:`, repoError);
|
|
1774
|
-
// continue with other repos
|
|
1775
|
-
}
|
|
1776
|
-
}
|
|
1777
|
-
return {
|
|
1778
|
-
success: true,
|
|
1779
|
-
repositories,
|
|
1780
|
-
overallSummary: {
|
|
1781
|
-
totalRepositories: repositories.length,
|
|
1782
|
-
totalFiles: overallFiles,
|
|
1783
|
-
totalInsertions: overallInsertions,
|
|
1784
|
-
totalDeletions: overallDeletions,
|
|
1785
|
-
totalChanges: overallChanges
|
|
1786
|
-
}
|
|
1787
|
-
};
|
|
1788
|
-
}
|
|
1789
|
-
catch (error) {
|
|
1790
|
-
logger.error('[getGitChanges] Error:', error);
|
|
1791
|
-
return {
|
|
1792
|
-
success: false,
|
|
1793
|
-
error: error instanceof Error ? error.message : 'Unknown error occurred while getting git changes'
|
|
1794
|
-
};
|
|
1795
|
-
}
|
|
1796
|
-
}
|
|
1797
|
-
/**
|
|
1798
|
-
* Get the unified diff for a single file in a repository.
|
|
1799
|
-
* status codes: 'M' modified, 'MM' modified+staged, 'A' added/staged,
|
|
1800
|
-
* '?' untracked, 'D' deleted, 'R' renamed
|
|
1801
|
-
*/
|
|
1802
|
-
async getFileDiff(repoName, filePath, status) {
|
|
1803
|
-
const pathModule = await Promise.resolve().then(() => __importStar(require('path')));
|
|
1804
|
-
const fs = await Promise.resolve().then(() => __importStar(require('fs')));
|
|
1805
|
-
logger.info(`[getFileDiff] repoName: ${repoName} | filePath: ${filePath} | status: ${status}`);
|
|
1806
|
-
try {
|
|
1807
|
-
let repoPath = pathModule.isAbsolute(repoName)
|
|
1808
|
-
? repoName
|
|
1809
|
-
: pathModule.join(tool_server_1.folderPath, repoName);
|
|
1810
|
-
if (!fs.existsSync(repoPath)) {
|
|
1811
|
-
return { success: false, error: `Repository path does not exist: ${repoPath}`, filePath, repoPath };
|
|
1812
|
-
}
|
|
1813
|
-
const result = await getSingleFileDiff(repoPath, filePath, status);
|
|
1814
|
-
logger.success(`[getFileDiff] Retrieved diff for ${filePath} (${(result.diff || '').length} chars)`);
|
|
1815
|
-
return result;
|
|
1816
|
-
}
|
|
1817
|
-
catch (error) {
|
|
1818
|
-
logger.error('[getFileDiff] Error:', error);
|
|
1819
|
-
return {
|
|
1820
|
-
success: false,
|
|
1821
|
-
error: error instanceof Error ? error.message : 'Unknown error occurred while getting file diff',
|
|
1822
|
-
filePath,
|
|
1823
|
-
repoPath: repoName
|
|
1824
|
-
};
|
|
1825
|
-
}
|
|
1826
|
-
}
|
|
1827
|
-
/**
|
|
1828
|
-
* Discard changes for a single file in a repository.
|
|
1829
|
-
* - Untracked / created files: physically deleted from disk.
|
|
1830
|
-
* - Modified / staged / deleted files: unstaged if needed, then restored from HEAD.
|
|
1831
|
-
*/
|
|
1832
|
-
async discardFileChanges(repoName, filePath) {
|
|
1833
|
-
const pathModule = await Promise.resolve().then(() => __importStar(require('path')));
|
|
1834
|
-
const fs = await Promise.resolve().then(() => __importStar(require('fs')));
|
|
1835
|
-
logger.info(`[discardFileChanges] repoName: ${repoName} | filePath: ${filePath}`);
|
|
1836
|
-
try {
|
|
1837
|
-
const repoPath = pathModule.isAbsolute(repoName)
|
|
1838
|
-
? repoName
|
|
1839
|
-
: pathModule.join(tool_server_1.folderPath, repoName);
|
|
1840
|
-
if (!fs.existsSync(repoPath)) {
|
|
1841
|
-
return { success: false, error: `Repository path does not exist: ${repoPath}` };
|
|
1842
|
-
}
|
|
1843
|
-
const gitDir = pathModule.join(repoPath, '.git');
|
|
1844
|
-
if (!fs.existsSync(gitDir)) {
|
|
1845
|
-
return { success: false, error: `Not a git repository: ${repoPath}` };
|
|
1846
|
-
}
|
|
1847
|
-
const git = (0, simple_git_1.simpleGit)(repoPath);
|
|
1848
|
-
const status = await git.status();
|
|
1849
|
-
const isModified = status.modified.includes(filePath);
|
|
1850
|
-
const isStaged = status.staged.includes(filePath);
|
|
1851
|
-
const isDeleted = status.deleted.includes(filePath);
|
|
1852
|
-
const isCreated = status.not_added.includes(filePath) || ((status.created || []).includes(filePath));
|
|
1853
|
-
const fullFilePath = pathModule.join(repoPath, filePath);
|
|
1854
|
-
const fileExistsOnDisk = fs.existsSync(fullFilePath);
|
|
1855
|
-
logger.info(`[discardFileChanges] flags — modified:${isModified} staged:${isStaged} deleted:${isDeleted} created:${isCreated} existsOnDisk:${fileExistsOnDisk}`);
|
|
1856
|
-
if (isCreated) {
|
|
1857
|
-
// Untracked file — remove from disk
|
|
1858
|
-
if (fileExistsOnDisk) {
|
|
1859
|
-
await fs.promises.unlink(fullFilePath);
|
|
1860
|
-
logger.success(`[discardFileChanges] Untracked file removed: ${filePath}`);
|
|
1861
|
-
return { success: true, message: `Untracked file '${filePath}' has been removed.` };
|
|
1862
|
-
}
|
|
1863
|
-
else {
|
|
1864
|
-
return { success: false, error: `File '${filePath}' does not exist on disk.` };
|
|
1865
|
-
}
|
|
1866
|
-
}
|
|
1867
|
-
if (isModified || isStaged || isDeleted) {
|
|
1868
|
-
try {
|
|
1869
|
-
if (isStaged) {
|
|
1870
|
-
await git.reset(['HEAD', filePath]);
|
|
1871
|
-
logger.info(`[discardFileChanges] Unstaged: ${filePath}`);
|
|
1872
|
-
}
|
|
1873
|
-
await git.checkout(['HEAD', '--', filePath]);
|
|
1874
|
-
logger.success(`[discardFileChanges] Restored from HEAD: ${filePath}`);
|
|
1875
|
-
return { success: true, message: `Changes to '${filePath}' have been discarded.` };
|
|
1876
|
-
}
|
|
1877
|
-
catch (checkoutError) {
|
|
1878
|
-
logger.error('[discardFileChanges] git checkout error:', checkoutError);
|
|
1879
|
-
return { success: false, error: `Failed to discard changes: ${checkoutError.message}` };
|
|
1880
|
-
}
|
|
1881
|
-
}
|
|
1882
|
-
return { success: false, error: `File '${filePath}' has no changes to discard.` };
|
|
1883
|
-
}
|
|
1884
|
-
catch (error) {
|
|
1885
|
-
logger.error('[discardFileChanges] Error:', error);
|
|
1886
|
-
return {
|
|
1887
|
-
success: false,
|
|
1888
|
-
error: error instanceof Error ? error.message : 'Unknown error occurred while discarding changes'
|
|
1889
|
-
};
|
|
1890
|
-
}
|
|
1891
|
-
}
|
|
1892
1961
|
}
|
|
1893
1962
|
exports.GithubOperationsService = GithubOperationsService;
|
|
1894
1963
|
//# sourceMappingURL=githubOperationsHanlder.js.map
|