phantomx-tool-client 1.0.7 → 1.0.9
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/__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 +365 -233
- 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 +172 -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.
|
|
@@ -127,6 +129,34 @@ function getFileStats(fileStats) {
|
|
|
127
129
|
}
|
|
128
130
|
return { insertions: 0, deletions: 0 };
|
|
129
131
|
}
|
|
132
|
+
// Helper: recursively find all git repositories under a root path (max depth 10)
|
|
133
|
+
async function findGitRepositories(rootPath) {
|
|
134
|
+
const fs = await Promise.resolve().then(() => __importStar(require('fs')));
|
|
135
|
+
const pathModule = await Promise.resolve().then(() => __importStar(require('path')));
|
|
136
|
+
const repositories = [];
|
|
137
|
+
async function searchDirectory(currentPath, depth = 0) {
|
|
138
|
+
if (depth > 10)
|
|
139
|
+
return;
|
|
140
|
+
try {
|
|
141
|
+
const items = await fs.promises.readdir(currentPath, { withFileTypes: true });
|
|
142
|
+
const gitPath = pathModule.join(currentPath, '.git');
|
|
143
|
+
if (fs.existsSync(gitPath)) {
|
|
144
|
+
repositories.push(currentPath);
|
|
145
|
+
return; // don't recurse inside a git repo looking for nested repos
|
|
146
|
+
}
|
|
147
|
+
for (const item of items) {
|
|
148
|
+
if (item.isDirectory() && !item.name.startsWith('.') && item.name !== 'node_modules') {
|
|
149
|
+
await searchDirectory(pathModule.join(currentPath, item.name), depth + 1);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
catch {
|
|
154
|
+
// skip directories we can't read
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
await searchDirectory(rootPath);
|
|
158
|
+
return repositories;
|
|
159
|
+
}
|
|
130
160
|
async function getRepositoryChangesMetadataOnly(repoPath) {
|
|
131
161
|
const pathModule = await Promise.resolve().then(() => __importStar(require('path')));
|
|
132
162
|
const fs = await Promise.resolve().then(() => __importStar(require('fs')));
|
|
@@ -285,6 +315,246 @@ async function getSingleFileDiff(repoPath, filePath, status) {
|
|
|
285
315
|
}
|
|
286
316
|
}
|
|
287
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
|
+
}
|
|
288
558
|
escapeSingleQuotedShell(value) {
|
|
289
559
|
return value.replace(/'/g, `'\\''`);
|
|
290
560
|
}
|
|
@@ -411,21 +681,21 @@ class GithubOperationsService {
|
|
|
411
681
|
try {
|
|
412
682
|
logger.info('Starting git push operation', { repoName });
|
|
413
683
|
// Combine all commands into a single SSH call
|
|
414
|
-
const combinedCommand = `
|
|
415
|
-
cd ${repoPath} &&
|
|
416
|
-
BRANCH=$(sudo git rev-parse --abbrev-ref HEAD) &&
|
|
417
|
-
COMMITS_AHEAD=$(sudo git rev-list --count origin/$BRANCH..HEAD 2>/dev/null || echo 0) &&
|
|
418
|
-
if [ "$COMMITS_AHEAD" = "0" ]; then
|
|
419
|
-
echo "ERROR:NO_COMMITS";
|
|
420
|
-
exit 1;
|
|
421
|
-
fi &&
|
|
422
|
-
sudo git remote set-url origin https://x-access-token:${installationToken}@github.com/${gitOrgName}/${repoName}.git &&
|
|
423
|
-
sudo git push origin $BRANCH &&
|
|
424
|
-
sudo git remote set-url origin https://github.com/${gitOrgName}/${repoName}.git &&
|
|
425
|
-
COMMIT_HASH=$(sudo git rev-parse HEAD) &&
|
|
426
|
-
echo "BRANCH:$BRANCH" &&
|
|
427
|
-
echo "COMMITS_AHEAD:$COMMITS_AHEAD" &&
|
|
428
|
-
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"
|
|
429
699
|
`.replace(/\n\s+/g, ' ');
|
|
430
700
|
const result = await executeShell(combinedCommand);
|
|
431
701
|
if (!result.success) {
|
|
@@ -468,23 +738,23 @@ class GithubOperationsService {
|
|
|
468
738
|
try {
|
|
469
739
|
logger.info('Checking repository status', { repoName });
|
|
470
740
|
// Combine all git commands into a single SSH call
|
|
471
|
-
const combinedCommand = `
|
|
472
|
-
cd ${repoPath} &&
|
|
473
|
-
BRANCH=$(sudo git rev-parse --abbrev-ref HEAD) &&
|
|
474
|
-
sudo git remote set-url origin https://x-access-token:${installationToken}@github.com/${gitOrgName}/${repoName}.git &&
|
|
475
|
-
sudo git fetch origin &&
|
|
476
|
-
sudo git remote set-url origin https://github.com/${gitOrgName}/${repoName}.git &&
|
|
477
|
-
BEHIND=$(sudo git rev-list --count HEAD..origin/$BRANCH 2>/dev/null || echo 0) &&
|
|
478
|
-
AHEAD=$(sudo git rev-list --count origin/$BRANCH..HEAD 2>/dev/null || echo 0) &&
|
|
479
|
-
STATUS=$(sudo git status --porcelain) &&
|
|
480
|
-
CURRENT_HASH=$(sudo git rev-parse HEAD) &&
|
|
481
|
-
REMOTE_HASH=$(sudo git rev-parse origin/$BRANCH 2>/dev/null || echo unknown) &&
|
|
482
|
-
echo "BRANCH:$BRANCH" &&
|
|
483
|
-
echo "BEHIND:$BEHIND" &&
|
|
484
|
-
echo "AHEAD:$AHEAD" &&
|
|
485
|
-
echo "STATUS:$STATUS" &&
|
|
486
|
-
echo "CURRENT:$CURRENT_HASH" &&
|
|
487
|
-
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"
|
|
488
758
|
`.replace(/\n\s+/g, ' ');
|
|
489
759
|
const result = await executeShell(combinedCommand);
|
|
490
760
|
if (!result.success) {
|
|
@@ -681,23 +951,23 @@ class GithubOperationsService {
|
|
|
681
951
|
const escapedAuthorName = commitAuthorName.replace(/"/g, '\\"');
|
|
682
952
|
const escapedAuthorEmail = commitAuthorEmail.replace(/"/g, '\\"');
|
|
683
953
|
// Combine all commands into a single SSH call
|
|
684
|
-
const combinedCommand = `
|
|
685
|
-
cd ${repoPath} &&
|
|
686
|
-
sudo git config user.name "${escapedAuthorName}" &&
|
|
687
|
-
sudo git config user.email "${escapedAuthorEmail}" &&
|
|
688
|
-
STATUS=$(sudo git status --porcelain) &&
|
|
689
|
-
if [ -z "$STATUS" ]; then
|
|
690
|
-
echo "ERROR:NO_CHANGES";
|
|
691
|
-
exit 1;
|
|
692
|
-
fi &&
|
|
693
|
-
CHANGED_FILES=$(echo "$STATUS" | wc -l) &&
|
|
694
|
-
sudo git add . &&
|
|
695
|
-
sudo git commit -m "${escapedCommitMsg}" &&
|
|
696
|
-
COMMIT_HASH=$(sudo git rev-parse HEAD) &&
|
|
697
|
-
SHORT_HASH=$(sudo git rev-parse --short HEAD) &&
|
|
698
|
-
echo "CHANGED_FILES:$CHANGED_FILES" &&
|
|
699
|
-
echo "COMMIT_HASH:$COMMIT_HASH" &&
|
|
700
|
-
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"
|
|
701
971
|
`.replace(/\n\s+/g, ' ');
|
|
702
972
|
const result = await executeShell(combinedCommand);
|
|
703
973
|
if (!result.success) {
|
|
@@ -782,20 +1052,20 @@ class GithubOperationsService {
|
|
|
782
1052
|
try {
|
|
783
1053
|
logger.info('Creating pull request', { repoName, targetBranch });
|
|
784
1054
|
// Combine commands into a single SSH call, with remote URL set/unset
|
|
785
|
-
const combinedCommand = `
|
|
786
|
-
cd ${repoPath} &&
|
|
787
|
-
sudo git remote set-url origin https://x-access-token:${installationToken}@github.com/${gitOrgName}/${repoName}.git &&
|
|
788
|
-
HEAD_BRANCH=$(sudo git rev-parse --abbrev-ref HEAD) &&
|
|
789
|
-
sudo git fetch origin ${targetBranch} 2>/dev/null &&
|
|
790
|
-
COMMITS_AHEAD=$(sudo git rev-list --count origin/${targetBranch}..$HEAD_BRANCH 2>/dev/null || echo 0) &&
|
|
791
|
-
if [ "$COMMITS_AHEAD" = "0" ]; then
|
|
792
|
-
echo "ERROR:NO_COMMITS:$HEAD_BRANCH";
|
|
793
|
-
sudo git remote set-url origin https://github.com/${gitOrgName}/${repoName}.git;
|
|
794
|
-
exit 1;
|
|
795
|
-
fi &&
|
|
796
|
-
echo "HEAD_BRANCH:$HEAD_BRANCH" &&
|
|
797
|
-
echo "COMMITS_AHEAD:$COMMITS_AHEAD" &&
|
|
798
|
-
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
|
|
799
1069
|
`.replace(/\n\s+/g, ' ');
|
|
800
1070
|
let result = await executeShell(combinedCommand);
|
|
801
1071
|
const output = result.output || '';
|
|
@@ -992,20 +1262,20 @@ class GithubOperationsService {
|
|
|
992
1262
|
const targetLocalBranch = resolvedTarget.displayBranch;
|
|
993
1263
|
const safeTargetLocalBranch = this.escapeSingleQuotedShell(targetLocalBranch);
|
|
994
1264
|
const safeSourceRef = this.escapeSingleQuotedShell(resolvedSource.resolvedRef);
|
|
995
|
-
const prepareBranchCommand = `
|
|
996
|
-
cd ${repoPath} &&
|
|
997
|
-
TARGET_LOCAL='${safeTargetLocalBranch}' &&
|
|
998
|
-
if sudo git show-ref --verify --quiet "refs/heads/$TARGET_LOCAL"; then
|
|
999
|
-
sudo git checkout "$TARGET_LOCAL";
|
|
1000
|
-
elif sudo git show-ref --verify --quiet "refs/remotes/origin/$TARGET_LOCAL"; then
|
|
1001
|
-
sudo git checkout -b "$TARGET_LOCAL" "origin/$TARGET_LOCAL";
|
|
1002
|
-
else
|
|
1003
|
-
echo "ERROR:TARGET_BRANCH_NOT_CHECKOUTABLE";
|
|
1004
|
-
exit 1;
|
|
1005
|
-
fi &&
|
|
1006
|
-
if sudo git show-ref --verify --quiet "refs/remotes/origin/$TARGET_LOCAL"; then
|
|
1007
|
-
sudo git pull --ff-only origin "$TARGET_LOCAL" 2>/dev/null || true;
|
|
1008
|
-
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
|
|
1009
1279
|
`.replace(/\n\s+/g, ' ');
|
|
1010
1280
|
result = await executeShell(prepareBranchCommand);
|
|
1011
1281
|
if (!result.success) {
|
|
@@ -1103,16 +1373,16 @@ class GithubOperationsService {
|
|
|
1103
1373
|
try {
|
|
1104
1374
|
logger.info('Checking if PR exists', { repoName, targetBranch });
|
|
1105
1375
|
// Get current branch and commits ahead
|
|
1106
|
-
const combinedCommand = `
|
|
1107
|
-
cd ${repoPath} &&
|
|
1108
|
-
sudo git remote set-url origin https://x-access-token:${installationToken}@github.com/${gitOrgName}/${repoName}.git &&
|
|
1109
|
-
HEAD_BRANCH=$(sudo git rev-parse --abbrev-ref HEAD) &&
|
|
1110
|
-
sudo git fetch origin --prune 2>/dev/null || true &&
|
|
1111
|
-
sudo git fetch origin ${targetBranch} 2>/dev/null || true &&
|
|
1112
|
-
COMMITS_AHEAD=$(sudo git rev-list --count origin/${targetBranch}..$HEAD_BRANCH 2>/dev/null || echo 0) &&
|
|
1113
|
-
sudo git remote set-url origin https://github.com/${gitOrgName}/${repoName}.git &&
|
|
1114
|
-
echo "HEAD_BRANCH:$HEAD_BRANCH" &&
|
|
1115
|
-
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"
|
|
1116
1386
|
`.replace(/\n\s+/g, ' ');
|
|
1117
1387
|
const result = await executeShell(combinedCommand);
|
|
1118
1388
|
// Check for command execution failure
|
|
@@ -1387,14 +1657,14 @@ class GithubOperationsService {
|
|
|
1387
1657
|
try {
|
|
1388
1658
|
logger.info('Fetching commit details', { repoName, commitHash });
|
|
1389
1659
|
// Get commit information and file changes in a single SSH call
|
|
1390
|
-
const combinedCommand = `
|
|
1391
|
-
cd ${repoPath} &&
|
|
1392
|
-
COMMIT_INFO=$(sudo git show --no-patch --pretty=format:"%H|%h|%an|%ae|%ad|%s" --date=iso ${commitHash}) &&
|
|
1393
|
-
FILE_STATS=$(sudo git show --stat --pretty="" --numstat ${commitHash}) &&
|
|
1394
|
-
echo "COMMIT_INFO:$COMMIT_INFO" &&
|
|
1395
|
-
echo "FILE_STATS_START" &&
|
|
1396
|
-
echo "$FILE_STATS" &&
|
|
1397
|
-
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"
|
|
1398
1668
|
`.replace(/\n\s+/g, ' ');
|
|
1399
1669
|
const result = await executeShell(combinedCommand);
|
|
1400
1670
|
if (!result.success) {
|
|
@@ -1688,144 +1958,6 @@ class GithubOperationsService {
|
|
|
1688
1958
|
});
|
|
1689
1959
|
});
|
|
1690
1960
|
}
|
|
1691
|
-
// ── New git operations ─────────────────────────────────────────────────────
|
|
1692
|
-
/**
|
|
1693
|
-
* Get all uncommitted git changes (metadata only, no diffs) for a repository.
|
|
1694
|
-
* Accepts repoName (looked up under folderPath) or a full absolute repoPath.
|
|
1695
|
-
*/
|
|
1696
|
-
async getGitChanges(repoName) {
|
|
1697
|
-
const pathModule = await Promise.resolve().then(() => __importStar(require('path')));
|
|
1698
|
-
const fs = await Promise.resolve().then(() => __importStar(require('fs')));
|
|
1699
|
-
logger.info(`[getGitChanges] repoName: ${repoName}`);
|
|
1700
|
-
try {
|
|
1701
|
-
// Resolve the repo path: treat as absolute if it starts with '/', otherwise join with folderPath
|
|
1702
|
-
let repoPath = pathModule.isAbsolute(repoName)
|
|
1703
|
-
? repoName
|
|
1704
|
-
: pathModule.join(tool_server_1.folderPath, repoName);
|
|
1705
|
-
if (!fs.existsSync(repoPath)) {
|
|
1706
|
-
return { success: false, error: `Repository path does not exist: ${repoPath}` };
|
|
1707
|
-
}
|
|
1708
|
-
const gitDir = pathModule.join(repoPath, '.git');
|
|
1709
|
-
if (!fs.existsSync(gitDir)) {
|
|
1710
|
-
return { success: false, error: `Not a git repository: ${repoPath}` };
|
|
1711
|
-
}
|
|
1712
|
-
logger.info(`[getGitChanges] Fetching metadata for repo: ${repoPath}`);
|
|
1713
|
-
const repoData = await getRepositoryChangesMetadataOnly(repoPath);
|
|
1714
|
-
return {
|
|
1715
|
-
success: true,
|
|
1716
|
-
repositories: [repoData],
|
|
1717
|
-
overallSummary: {
|
|
1718
|
-
totalRepositories: 1,
|
|
1719
|
-
totalFiles: repoData.summary.totalFiles,
|
|
1720
|
-
totalInsertions: repoData.summary.totalInsertions,
|
|
1721
|
-
totalDeletions: repoData.summary.totalDeletions,
|
|
1722
|
-
totalChanges: repoData.summary.totalChanges
|
|
1723
|
-
}
|
|
1724
|
-
};
|
|
1725
|
-
}
|
|
1726
|
-
catch (error) {
|
|
1727
|
-
logger.error('[getGitChanges] Error:', error);
|
|
1728
|
-
return {
|
|
1729
|
-
success: false,
|
|
1730
|
-
error: error instanceof Error ? error.message : 'Unknown error occurred while getting git changes'
|
|
1731
|
-
};
|
|
1732
|
-
}
|
|
1733
|
-
}
|
|
1734
|
-
/**
|
|
1735
|
-
* Get the unified diff for a single file in a repository.
|
|
1736
|
-
* status codes: 'M' modified, 'MM' modified+staged, 'A' added/staged,
|
|
1737
|
-
* '?' untracked, 'D' deleted, 'R' renamed
|
|
1738
|
-
*/
|
|
1739
|
-
async getFileDiff(repoName, filePath, status) {
|
|
1740
|
-
const pathModule = await Promise.resolve().then(() => __importStar(require('path')));
|
|
1741
|
-
const fs = await Promise.resolve().then(() => __importStar(require('fs')));
|
|
1742
|
-
logger.info(`[getFileDiff] repoName: ${repoName} | filePath: ${filePath} | status: ${status}`);
|
|
1743
|
-
try {
|
|
1744
|
-
let repoPath = pathModule.isAbsolute(repoName)
|
|
1745
|
-
? repoName
|
|
1746
|
-
: pathModule.join(tool_server_1.folderPath, repoName);
|
|
1747
|
-
if (!fs.existsSync(repoPath)) {
|
|
1748
|
-
return { success: false, error: `Repository path does not exist: ${repoPath}`, filePath, repoPath };
|
|
1749
|
-
}
|
|
1750
|
-
const result = await getSingleFileDiff(repoPath, filePath, status);
|
|
1751
|
-
logger.success(`[getFileDiff] Retrieved diff for ${filePath} (${(result.diff || '').length} chars)`);
|
|
1752
|
-
return result;
|
|
1753
|
-
}
|
|
1754
|
-
catch (error) {
|
|
1755
|
-
logger.error('[getFileDiff] Error:', error);
|
|
1756
|
-
return {
|
|
1757
|
-
success: false,
|
|
1758
|
-
error: error instanceof Error ? error.message : 'Unknown error occurred while getting file diff',
|
|
1759
|
-
filePath,
|
|
1760
|
-
repoPath: repoName
|
|
1761
|
-
};
|
|
1762
|
-
}
|
|
1763
|
-
}
|
|
1764
|
-
/**
|
|
1765
|
-
* Discard changes for a single file in a repository.
|
|
1766
|
-
* - Untracked / created files: physically deleted from disk.
|
|
1767
|
-
* - Modified / staged / deleted files: unstaged if needed, then restored from HEAD.
|
|
1768
|
-
*/
|
|
1769
|
-
async discardFileChanges(repoName, filePath) {
|
|
1770
|
-
const pathModule = await Promise.resolve().then(() => __importStar(require('path')));
|
|
1771
|
-
const fs = await Promise.resolve().then(() => __importStar(require('fs')));
|
|
1772
|
-
logger.info(`[discardFileChanges] repoName: ${repoName} | filePath: ${filePath}`);
|
|
1773
|
-
try {
|
|
1774
|
-
const repoPath = pathModule.isAbsolute(repoName)
|
|
1775
|
-
? repoName
|
|
1776
|
-
: pathModule.join(tool_server_1.folderPath, repoName);
|
|
1777
|
-
if (!fs.existsSync(repoPath)) {
|
|
1778
|
-
return { success: false, error: `Repository path does not exist: ${repoPath}` };
|
|
1779
|
-
}
|
|
1780
|
-
const gitDir = pathModule.join(repoPath, '.git');
|
|
1781
|
-
if (!fs.existsSync(gitDir)) {
|
|
1782
|
-
return { success: false, error: `Not a git repository: ${repoPath}` };
|
|
1783
|
-
}
|
|
1784
|
-
const git = (0, simple_git_1.simpleGit)(repoPath);
|
|
1785
|
-
const status = await git.status();
|
|
1786
|
-
const isModified = status.modified.includes(filePath);
|
|
1787
|
-
const isStaged = status.staged.includes(filePath);
|
|
1788
|
-
const isDeleted = status.deleted.includes(filePath);
|
|
1789
|
-
const isCreated = status.not_added.includes(filePath) || ((status.created || []).includes(filePath));
|
|
1790
|
-
const fullFilePath = pathModule.join(repoPath, filePath);
|
|
1791
|
-
const fileExistsOnDisk = fs.existsSync(fullFilePath);
|
|
1792
|
-
logger.info(`[discardFileChanges] flags — modified:${isModified} staged:${isStaged} deleted:${isDeleted} created:${isCreated} existsOnDisk:${fileExistsOnDisk}`);
|
|
1793
|
-
if (isCreated) {
|
|
1794
|
-
// Untracked file — remove from disk
|
|
1795
|
-
if (fileExistsOnDisk) {
|
|
1796
|
-
await fs.promises.unlink(fullFilePath);
|
|
1797
|
-
logger.success(`[discardFileChanges] Untracked file removed: ${filePath}`);
|
|
1798
|
-
return { success: true, message: `Untracked file '${filePath}' has been removed.` };
|
|
1799
|
-
}
|
|
1800
|
-
else {
|
|
1801
|
-
return { success: false, error: `File '${filePath}' does not exist on disk.` };
|
|
1802
|
-
}
|
|
1803
|
-
}
|
|
1804
|
-
if (isModified || isStaged || isDeleted) {
|
|
1805
|
-
try {
|
|
1806
|
-
if (isStaged) {
|
|
1807
|
-
await git.reset(['HEAD', filePath]);
|
|
1808
|
-
logger.info(`[discardFileChanges] Unstaged: ${filePath}`);
|
|
1809
|
-
}
|
|
1810
|
-
await git.checkout(['HEAD', '--', filePath]);
|
|
1811
|
-
logger.success(`[discardFileChanges] Restored from HEAD: ${filePath}`);
|
|
1812
|
-
return { success: true, message: `Changes to '${filePath}' have been discarded.` };
|
|
1813
|
-
}
|
|
1814
|
-
catch (checkoutError) {
|
|
1815
|
-
logger.error('[discardFileChanges] git checkout error:', checkoutError);
|
|
1816
|
-
return { success: false, error: `Failed to discard changes: ${checkoutError.message}` };
|
|
1817
|
-
}
|
|
1818
|
-
}
|
|
1819
|
-
return { success: false, error: `File '${filePath}' has no changes to discard.` };
|
|
1820
|
-
}
|
|
1821
|
-
catch (error) {
|
|
1822
|
-
logger.error('[discardFileChanges] Error:', error);
|
|
1823
|
-
return {
|
|
1824
|
-
success: false,
|
|
1825
|
-
error: error instanceof Error ? error.message : 'Unknown error occurred while discarding changes'
|
|
1826
|
-
};
|
|
1827
|
-
}
|
|
1828
|
-
}
|
|
1829
1961
|
}
|
|
1830
1962
|
exports.GithubOperationsService = GithubOperationsService;
|
|
1831
1963
|
//# sourceMappingURL=githubOperationsHanlder.js.map
|