phantomx-tool-client 2.1.0 → 2.1.2
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/githubOperationsHanlder.d.ts +1 -1
- package/dist/githubOperationsHanlder.d.ts.map +1 -1
- package/dist/githubOperationsHanlder.js +493 -462
- package/dist/githubOperationsHanlder.js.map +1 -1
- package/dist/tool-server.d.ts +2 -0
- package/dist/tool-server.d.ts.map +1 -1
- package/dist/tool-server.js +16 -15
- package/dist/tool-server.js.map +1 -1
- package/dist/toolExecutionService.d.ts.map +1 -1
- package/dist/toolExecutionService.js +33 -0
- package/dist/toolExecutionService.js.map +1 -1
- package/package.json +5 -2
|
@@ -64,6 +64,19 @@ function executeShell(command) {
|
|
|
64
64
|
});
|
|
65
65
|
});
|
|
66
66
|
}
|
|
67
|
+
// ---------------------------------------------------------------------------
|
|
68
|
+
// Per-repo async mutex — serialises all git remote set-url operations so that
|
|
69
|
+
// concurrent tool calls never race on .git/config.lock for the same repo.
|
|
70
|
+
// ---------------------------------------------------------------------------
|
|
71
|
+
const repoLocks = new Map();
|
|
72
|
+
function withRepoLock(repoName, fn) {
|
|
73
|
+
var _a;
|
|
74
|
+
const prev = (_a = repoLocks.get(repoName)) !== null && _a !== void 0 ? _a : Promise.resolve();
|
|
75
|
+
let releaseLock;
|
|
76
|
+
const lockAcquired = new Promise(res => { releaseLock = res; });
|
|
77
|
+
repoLocks.set(repoName, prev.then(() => lockAcquired));
|
|
78
|
+
return prev.then(() => fn()).finally(() => releaseLock());
|
|
79
|
+
}
|
|
67
80
|
// Use the official 'octokit' package import via require to avoid missing types for '@octokit/rest'
|
|
68
81
|
const { Octokit } = require('octokit');
|
|
69
82
|
const logger = (0, Logger_1.createLogger)('GithubOperationsService');
|
|
@@ -597,25 +610,26 @@ class GithubOperationsService {
|
|
|
597
610
|
const repoPath = `${tool_server_1.folderPath}/${repoName}`;
|
|
598
611
|
const installationToken = await fetchInstallationToken();
|
|
599
612
|
const gitOrgName = await getGithubOrganizationName();
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
613
|
+
return withRepoLock(repoName, async () => {
|
|
614
|
+
try {
|
|
615
|
+
logger.info('Getting repository origin/default branch', { repoName });
|
|
616
|
+
// Set token URL, query origin, restore URL — single pipeline to prevent lock races.
|
|
617
|
+
const result = await executeShell(`cd ${repoPath} && rm -f .git/config.lock && ` +
|
|
618
|
+
`git remote set-url origin https://x-access-token:${installationToken}@github.com/${gitOrgName}/${repoName}.git && ` +
|
|
619
|
+
`sudo git remote show origin | grep 'HEAD branch' | awk '{print $NF}' && ` +
|
|
620
|
+
`git remote set-url origin https://github.com/${gitOrgName}/${repoName}.git`);
|
|
621
|
+
if (!result.success || !result.output.trim()) {
|
|
622
|
+
throw new Error(`Failed to get origin branch for repo '${repoName}': ${result.error || result.output}`);
|
|
623
|
+
}
|
|
624
|
+
const originBranch = result.output.trim();
|
|
625
|
+
logger.success('Repository origin branch retrieved', { repoName, originBranch });
|
|
626
|
+
return {
|
|
627
|
+
repoName,
|
|
628
|
+
originBranch
|
|
629
|
+
};
|
|
630
|
+
}
|
|
631
|
+
finally { }
|
|
632
|
+
}); // end withRepoLock
|
|
619
633
|
}
|
|
620
634
|
/**
|
|
621
635
|
* List branches for a repository
|
|
@@ -636,40 +650,38 @@ class GithubOperationsService {
|
|
|
636
650
|
const repoPath = `${tool_server_1.folderPath}/${repoName}`;
|
|
637
651
|
const installationToken = await fetchInstallationToken();
|
|
638
652
|
const gitOrgName = await getGithubOrganizationName();
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
653
|
+
return withRepoLock(repoName, async () => {
|
|
654
|
+
try {
|
|
655
|
+
// Get current local branch name
|
|
656
|
+
let result = await executeShell(`cd ${repoPath} && sudo git rev-parse --abbrev-ref HEAD`);
|
|
657
|
+
if (!result.success) {
|
|
658
|
+
throw new Error(`Failed to get current branch: ${result.error}`);
|
|
659
|
+
}
|
|
660
|
+
const currentBranch = result.output.trim();
|
|
661
|
+
logger.info('starting git pull operation', { repoName, currentBranch });
|
|
662
|
+
// Token-URL, fetch, pull, restore URL — all in one pipeline to prevent
|
|
663
|
+
// concurrent operations from racing on .git/config.lock.
|
|
664
|
+
result = await executeShell(`cd ${repoPath} && rm -f .git/config.lock && ` +
|
|
665
|
+
`git remote set-url origin https://x-access-token:${installationToken}@github.com/${gitOrgName}/${repoName}.git && ` +
|
|
666
|
+
`sudo git fetch origin && ` +
|
|
667
|
+
`sudo git pull --no-rebase origin ${currentBranch} ; ` +
|
|
668
|
+
`git remote set-url origin https://github.com/${gitOrgName}/${repoName}.git`);
|
|
669
|
+
if (!result.success && result.code !== 128) {
|
|
670
|
+
throw new Error(`Failed to pull branch ${currentBranch}: ${result.error}`);
|
|
671
|
+
}
|
|
672
|
+
logger.success('Git pull completed successfully', { repoName, currentBranch });
|
|
673
|
+
// Get merge conflicts
|
|
674
|
+
const mergeConflicts = await this.getMergeConflicts(repoName);
|
|
675
|
+
return {
|
|
676
|
+
repoName,
|
|
677
|
+
branchName: currentBranch,
|
|
678
|
+
lastCommit: result.output.trim(),
|
|
679
|
+
mergeConflicts
|
|
680
|
+
};
|
|
655
681
|
}
|
|
656
|
-
|
|
657
|
-
if (!result.success && result.code !== 128) {
|
|
658
|
-
throw new Error(`Failed to pull branch ${currentBranch}: ${result.error}`);
|
|
682
|
+
finally {
|
|
659
683
|
}
|
|
660
|
-
|
|
661
|
-
logger.success('Git pull completed successfully', { repoName, currentBranch });
|
|
662
|
-
// Get merge conflicts
|
|
663
|
-
const mergeConflicts = await this.getMergeConflicts(socket, repoName);
|
|
664
|
-
return {
|
|
665
|
-
repoName,
|
|
666
|
-
branchName: currentBranch,
|
|
667
|
-
lastCommit: result.output.trim(),
|
|
668
|
-
mergeConflicts
|
|
669
|
-
};
|
|
670
|
-
}
|
|
671
|
-
finally {
|
|
672
|
-
}
|
|
684
|
+
}); // end withRepoLock
|
|
673
685
|
}
|
|
674
686
|
/**
|
|
675
687
|
* Push repository to remote
|
|
@@ -678,55 +690,58 @@ class GithubOperationsService {
|
|
|
678
690
|
const repoPath = `${tool_server_1.folderPath}/${repoName}`;
|
|
679
691
|
const installationToken = await fetchInstallationToken();
|
|
680
692
|
const gitOrgName = await getGithubOrganizationName();
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
693
|
+
return withRepoLock(repoName, async () => {
|
|
694
|
+
try {
|
|
695
|
+
logger.info('Starting git push operation', { repoName });
|
|
696
|
+
// Combine all commands into a single SSH call
|
|
697
|
+
const combinedCommand = `
|
|
685
698
|
cd ${repoPath} &&
|
|
699
|
+
rm -f .git/config.lock &&
|
|
686
700
|
BRANCH=$(sudo git rev-parse --abbrev-ref HEAD) &&
|
|
687
701
|
COMMITS_AHEAD=$(sudo git rev-list --count origin/$BRANCH..HEAD 2>/dev/null || echo 0) &&
|
|
688
702
|
if [ "$COMMITS_AHEAD" = "0" ]; then
|
|
689
703
|
echo "ERROR:NO_COMMITS";
|
|
690
704
|
exit 1;
|
|
691
705
|
fi &&
|
|
692
|
-
|
|
706
|
+
git remote set-url origin https://x-access-token:${installationToken}@github.com/${gitOrgName}/${repoName}.git &&
|
|
693
707
|
sudo git push origin $BRANCH &&
|
|
694
|
-
|
|
708
|
+
git remote set-url origin https://github.com/${gitOrgName}/${repoName}.git &&
|
|
695
709
|
COMMIT_HASH=$(sudo git rev-parse HEAD) &&
|
|
696
710
|
echo "BRANCH:$BRANCH" &&
|
|
697
711
|
echo "COMMITS_AHEAD:$COMMITS_AHEAD" &&
|
|
698
712
|
echo "COMMIT_HASH:$COMMIT_HASH"
|
|
699
713
|
`.replace(/\n\s+/g, ' ');
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
714
|
+
const result = await executeShell(combinedCommand);
|
|
715
|
+
if (!result.success) {
|
|
716
|
+
if (result.output && result.output.includes('ERROR:NO_COMMITS')) {
|
|
717
|
+
throw new Error('No commits to push');
|
|
718
|
+
}
|
|
719
|
+
throw new Error('Failed to push repository');
|
|
720
|
+
}
|
|
721
|
+
// Parse output
|
|
722
|
+
const output = result.output;
|
|
723
|
+
const branchMatch = output.match(/BRANCH:([^\n]+)/);
|
|
724
|
+
const commitsAheadMatch = output.match(/COMMITS_AHEAD:(\d+)/);
|
|
725
|
+
const commitHashMatch = output.match(/COMMIT_HASH:([a-f0-9]+)/);
|
|
726
|
+
const currentBranch = branchMatch ? branchMatch[1].trim() : 'unknown';
|
|
727
|
+
const commitsAhead = commitsAheadMatch ? parseInt(commitsAheadMatch[1]) : 0;
|
|
728
|
+
const commitHash = commitHashMatch ? commitHashMatch[1].trim() : 'unknown';
|
|
729
|
+
logger.success('Git push completed successfully', { repoName, currentBranch, commitHash, commitsAhead });
|
|
730
|
+
// Emit socket event to refresh repo on client (only when called by AI agent)
|
|
731
|
+
if (socket) {
|
|
732
|
+
socket.emit('refresh_repo', { repoName });
|
|
704
733
|
}
|
|
705
|
-
|
|
734
|
+
return {
|
|
735
|
+
repoName,
|
|
736
|
+
branchName: currentBranch,
|
|
737
|
+
commitHash,
|
|
738
|
+
commitsAhead,
|
|
739
|
+
pushedAt: new Date().toISOString()
|
|
740
|
+
};
|
|
706
741
|
}
|
|
707
|
-
|
|
708
|
-
const output = result.output;
|
|
709
|
-
const branchMatch = output.match(/BRANCH:([^\n]+)/);
|
|
710
|
-
const commitsAheadMatch = output.match(/COMMITS_AHEAD:(\d+)/);
|
|
711
|
-
const commitHashMatch = output.match(/COMMIT_HASH:([a-f0-9]+)/);
|
|
712
|
-
const currentBranch = branchMatch ? branchMatch[1].trim() : 'unknown';
|
|
713
|
-
const commitsAhead = commitsAheadMatch ? parseInt(commitsAheadMatch[1]) : 0;
|
|
714
|
-
const commitHash = commitHashMatch ? commitHashMatch[1].trim() : 'unknown';
|
|
715
|
-
logger.success('Git push completed successfully', { repoName, currentBranch, commitHash, commitsAhead });
|
|
716
|
-
// Emit socket event to refresh repo on client (only when called by AI agent)
|
|
717
|
-
if (socket) {
|
|
718
|
-
socket.emit('refresh_repo', { repoName });
|
|
742
|
+
finally {
|
|
719
743
|
}
|
|
720
|
-
|
|
721
|
-
repoName,
|
|
722
|
-
branchName: currentBranch,
|
|
723
|
-
commitHash,
|
|
724
|
-
commitsAhead,
|
|
725
|
-
pushedAt: new Date().toISOString()
|
|
726
|
-
};
|
|
727
|
-
}
|
|
728
|
-
finally {
|
|
729
|
-
}
|
|
744
|
+
}); // end withRepoLock
|
|
730
745
|
}
|
|
731
746
|
/**
|
|
732
747
|
* Check repository status
|
|
@@ -735,15 +750,21 @@ class GithubOperationsService {
|
|
|
735
750
|
const repoPath = `${tool_server_1.folderPath}/${repoName}`;
|
|
736
751
|
const installationToken = await fetchInstallationToken();
|
|
737
752
|
const gitOrgName = await getGithubOrganizationName();
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
753
|
+
return withRepoLock(repoName, async () => {
|
|
754
|
+
try {
|
|
755
|
+
logger.info('Checking repository status', { repoName });
|
|
756
|
+
// Combine all git commands into a single SSH call
|
|
757
|
+
// Remove any stale .git/config.lock before touching remote config
|
|
758
|
+
// (a crashed/interrupted previous run can leave this lock behind).
|
|
759
|
+
// Also avoid sudo for git remote set-url so the lock file is never
|
|
760
|
+
// created as root-owned, which would block subsequent cleanup.
|
|
761
|
+
const combinedCommand = `
|
|
742
762
|
cd ${repoPath} &&
|
|
763
|
+
rm -f .git/config.lock &&
|
|
743
764
|
BRANCH=$(sudo git rev-parse --abbrev-ref HEAD) &&
|
|
744
|
-
|
|
765
|
+
git remote set-url origin https://x-access-token:${installationToken}@github.com/${gitOrgName}/${repoName}.git &&
|
|
745
766
|
sudo git fetch origin &&
|
|
746
|
-
|
|
767
|
+
git remote set-url origin https://github.com/${gitOrgName}/${repoName}.git &&
|
|
747
768
|
BEHIND=$(sudo git rev-list --count HEAD..origin/$BRANCH 2>/dev/null || echo 0) &&
|
|
748
769
|
AHEAD=$(sudo git rev-list --count origin/$BRANCH..HEAD 2>/dev/null || echo 0) &&
|
|
749
770
|
STATUS=$(sudo git status --porcelain) &&
|
|
@@ -756,54 +777,55 @@ class GithubOperationsService {
|
|
|
756
777
|
echo "CURRENT:$CURRENT_HASH" &&
|
|
757
778
|
echo "REMOTE:$REMOTE_HASH"
|
|
758
779
|
`.replace(/\n\s+/g, ' ');
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
780
|
+
const result = await executeShell(combinedCommand);
|
|
781
|
+
if (!result.success) {
|
|
782
|
+
throw new Error('Failed to check repository status: ' + result.error);
|
|
783
|
+
}
|
|
784
|
+
// Parse the output
|
|
785
|
+
const output = result.output;
|
|
786
|
+
const branchMatch = output.match(/BRANCH:([^\n]+)/);
|
|
787
|
+
const behindMatch = output.match(/BEHIND:(\d+)/);
|
|
788
|
+
const aheadMatch = output.match(/AHEAD:(\d+)/);
|
|
789
|
+
const statusMatch = output.match(/STATUS:([\s\S]*?)(?=CURRENT:|$)/);
|
|
790
|
+
const currentMatch = output.match(/CURRENT:([a-f0-9]+)/);
|
|
791
|
+
const remoteMatch = output.match(/REMOTE:([a-f0-9]+|unknown)/);
|
|
792
|
+
const currentBranch = branchMatch ? branchMatch[1].trim() : 'unknown';
|
|
793
|
+
const commitsBehind = behindMatch ? parseInt(behindMatch[1]) : 0;
|
|
794
|
+
const commitsAhead = aheadMatch ? parseInt(aheadMatch[1]) : 0;
|
|
795
|
+
const statusOutput = statusMatch ? statusMatch[1].trim() : '';
|
|
796
|
+
const hasUncommittedChanges = statusOutput.length > 0;
|
|
797
|
+
const currentCommitHash = currentMatch ? currentMatch[1].trim() : 'unknown';
|
|
798
|
+
const remoteCommitHash = remoteMatch ? remoteMatch[1].trim() : 'unknown';
|
|
799
|
+
const isUpToDate = commitsBehind === 0 && commitsAhead === 0;
|
|
800
|
+
const needsPull = commitsBehind > 0;
|
|
801
|
+
const needsPush = commitsAhead > 0;
|
|
802
|
+
logger.info('Repository status checked', {
|
|
803
|
+
repoName,
|
|
804
|
+
commitsBehind,
|
|
805
|
+
commitsAhead,
|
|
806
|
+
hasUncommittedChanges,
|
|
807
|
+
isUpToDate
|
|
808
|
+
});
|
|
809
|
+
// Check for merge conflicts
|
|
810
|
+
const mergeConflicts = await this.getMergeConflicts(repoName);
|
|
811
|
+
return {
|
|
812
|
+
repoName,
|
|
813
|
+
branchName: currentBranch,
|
|
814
|
+
currentCommitHash,
|
|
815
|
+
remoteCommitHash,
|
|
816
|
+
commitsBehind,
|
|
817
|
+
commitsAhead,
|
|
818
|
+
hasUncommittedChanges,
|
|
819
|
+
isUpToDate,
|
|
820
|
+
needsPull,
|
|
821
|
+
needsPush,
|
|
822
|
+
mergeConflicts,
|
|
823
|
+
status: isUpToDate ? 'up-to-date' : (needsPull && needsPush ? 'diverged' : needsPull ? 'behind' : 'ahead')
|
|
824
|
+
};
|
|
762
825
|
}
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
const behindMatch = output.match(/BEHIND:(\d+)/);
|
|
767
|
-
const aheadMatch = output.match(/AHEAD:(\d+)/);
|
|
768
|
-
const statusMatch = output.match(/STATUS:([\s\S]*?)(?=CURRENT:|$)/);
|
|
769
|
-
const currentMatch = output.match(/CURRENT:([a-f0-9]+)/);
|
|
770
|
-
const remoteMatch = output.match(/REMOTE:([a-f0-9]+|unknown)/);
|
|
771
|
-
const currentBranch = branchMatch ? branchMatch[1].trim() : 'unknown';
|
|
772
|
-
const commitsBehind = behindMatch ? parseInt(behindMatch[1]) : 0;
|
|
773
|
-
const commitsAhead = aheadMatch ? parseInt(aheadMatch[1]) : 0;
|
|
774
|
-
const statusOutput = statusMatch ? statusMatch[1].trim() : '';
|
|
775
|
-
const hasUncommittedChanges = statusOutput.length > 0;
|
|
776
|
-
const currentCommitHash = currentMatch ? currentMatch[1].trim() : 'unknown';
|
|
777
|
-
const remoteCommitHash = remoteMatch ? remoteMatch[1].trim() : 'unknown';
|
|
778
|
-
const isUpToDate = commitsBehind === 0 && commitsAhead === 0;
|
|
779
|
-
const needsPull = commitsBehind > 0;
|
|
780
|
-
const needsPush = commitsAhead > 0;
|
|
781
|
-
logger.info('Repository status checked', {
|
|
782
|
-
repoName,
|
|
783
|
-
commitsBehind,
|
|
784
|
-
commitsAhead,
|
|
785
|
-
hasUncommittedChanges,
|
|
786
|
-
isUpToDate
|
|
787
|
-
});
|
|
788
|
-
// Check for merge conflicts
|
|
789
|
-
const mergeConflicts = await this.getMergeConflicts(socket, repoName);
|
|
790
|
-
return {
|
|
791
|
-
repoName,
|
|
792
|
-
branchName: currentBranch,
|
|
793
|
-
currentCommitHash,
|
|
794
|
-
remoteCommitHash,
|
|
795
|
-
commitsBehind,
|
|
796
|
-
commitsAhead,
|
|
797
|
-
hasUncommittedChanges,
|
|
798
|
-
isUpToDate,
|
|
799
|
-
needsPull,
|
|
800
|
-
needsPush,
|
|
801
|
-
mergeConflicts,
|
|
802
|
-
status: isUpToDate ? 'up-to-date' : (needsPull && needsPush ? 'diverged' : needsPull ? 'behind' : 'ahead')
|
|
803
|
-
};
|
|
804
|
-
}
|
|
805
|
-
finally {
|
|
806
|
-
}
|
|
826
|
+
finally {
|
|
827
|
+
}
|
|
828
|
+
}); // end withRepoLock
|
|
807
829
|
}
|
|
808
830
|
/**
|
|
809
831
|
* Get repository history
|
|
@@ -1049,183 +1071,186 @@ class GithubOperationsService {
|
|
|
1049
1071
|
const repoPath = `${tool_server_1.folderPath}/${repoName}`;
|
|
1050
1072
|
const installationToken = await fetchInstallationToken();
|
|
1051
1073
|
const gitOrgName = await getGithubOrganizationName();
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1074
|
+
return withRepoLock(repoName, async () => {
|
|
1075
|
+
try {
|
|
1076
|
+
logger.info('Creating pull request', { repoName, targetBranch });
|
|
1077
|
+
// Combine commands into a single SSH call, with remote URL set/unset
|
|
1078
|
+
const combinedCommand = `
|
|
1056
1079
|
cd ${repoPath} &&
|
|
1057
|
-
|
|
1080
|
+
rm -f .git/config.lock &&
|
|
1081
|
+
git remote set-url origin https://x-access-token:${installationToken}@github.com/${gitOrgName}/${repoName}.git &&
|
|
1058
1082
|
HEAD_BRANCH=$(sudo git rev-parse --abbrev-ref HEAD) &&
|
|
1059
1083
|
sudo git fetch origin ${targetBranch} 2>/dev/null &&
|
|
1060
1084
|
COMMITS_AHEAD=$(sudo git rev-list --count origin/${targetBranch}..$HEAD_BRANCH 2>/dev/null || echo 0) &&
|
|
1061
1085
|
if [ "$COMMITS_AHEAD" = "0" ]; then
|
|
1062
1086
|
echo "ERROR:NO_COMMITS:$HEAD_BRANCH";
|
|
1063
|
-
|
|
1087
|
+
git remote set-url origin https://github.com/${gitOrgName}/${repoName}.git;
|
|
1064
1088
|
exit 1;
|
|
1065
1089
|
fi &&
|
|
1066
1090
|
echo "HEAD_BRANCH:$HEAD_BRANCH" &&
|
|
1067
1091
|
echo "COMMITS_AHEAD:$COMMITS_AHEAD" &&
|
|
1068
|
-
|
|
1092
|
+
git remote set-url origin https://github.com/${gitOrgName}/${repoName}.git
|
|
1069
1093
|
`.replace(/\n\s+/g, ' ');
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
});
|
|
1085
|
-
// Always check for existing PR first even if there are no new commits
|
|
1086
|
-
try {
|
|
1087
|
-
const existingPRs = await octokit.rest.pulls.list({
|
|
1088
|
-
owner: gitOrgName,
|
|
1089
|
-
repo: repoName,
|
|
1090
|
-
head: `${gitOrgName}:${headBranch}`,
|
|
1091
|
-
base: targetBranch,
|
|
1092
|
-
state: 'open'
|
|
1094
|
+
let result = await executeShell(combinedCommand);
|
|
1095
|
+
const output = result.output || '';
|
|
1096
|
+
const headBranchMatch = output.match(/HEAD_BRANCH:([^\n]+)/);
|
|
1097
|
+
const commitsAheadMatch = output.match(/COMMITS_AHEAD:(\d+)/);
|
|
1098
|
+
const errorMatch = output.match(/ERROR:NO_COMMITS:([^\n]+)/);
|
|
1099
|
+
let headBranch = headBranchMatch ? headBranchMatch[1].trim() : 'unknown';
|
|
1100
|
+
const commitsAhead = commitsAheadMatch ? parseInt(commitsAheadMatch[1]) : 0;
|
|
1101
|
+
if (!result.success && errorMatch) {
|
|
1102
|
+
headBranch = errorMatch[1].trim();
|
|
1103
|
+
}
|
|
1104
|
+
// using 'octokit' package (already installed, @octokit/rest is not installed)
|
|
1105
|
+
const { Octokit } = require('octokit');
|
|
1106
|
+
const octokit = new Octokit({
|
|
1107
|
+
auth: installationToken
|
|
1093
1108
|
});
|
|
1094
|
-
if
|
|
1095
|
-
|
|
1096
|
-
|
|
1109
|
+
// Always check for existing PR first even if there are no new commits
|
|
1110
|
+
try {
|
|
1111
|
+
const existingPRs = await octokit.rest.pulls.list({
|
|
1112
|
+
owner: gitOrgName,
|
|
1113
|
+
repo: repoName,
|
|
1114
|
+
head: `${gitOrgName}:${headBranch}`,
|
|
1115
|
+
base: targetBranch,
|
|
1116
|
+
state: 'open'
|
|
1117
|
+
});
|
|
1118
|
+
if (existingPRs.data.length > 0) {
|
|
1119
|
+
const existingPR = existingPRs.data[0];
|
|
1120
|
+
logger.info('Pull request already exists', {
|
|
1121
|
+
repoName,
|
|
1122
|
+
prNumber: existingPR.number,
|
|
1123
|
+
prUrl: existingPR.html_url
|
|
1124
|
+
});
|
|
1125
|
+
const resultData = {
|
|
1126
|
+
repoName,
|
|
1127
|
+
headBranch,
|
|
1128
|
+
targetBranch,
|
|
1129
|
+
prNumber: existingPR.number,
|
|
1130
|
+
prUrl: existingPR.html_url,
|
|
1131
|
+
prTitle: existingPR.title,
|
|
1132
|
+
commitsAhead,
|
|
1133
|
+
createdAt: existingPR.created_at,
|
|
1134
|
+
alreadyExists: true
|
|
1135
|
+
};
|
|
1136
|
+
// Emit socket event to refresh PR status on client
|
|
1137
|
+
if (socket) {
|
|
1138
|
+
socket.emit('refresh_pr_status', { repoName, prNumber: existingPR.number });
|
|
1139
|
+
}
|
|
1140
|
+
// the socket server our main server will automatically update the pr metadata in db when i recvs the result of the given tool.
|
|
1141
|
+
// this.updatePRMetadata(userId, resultData);
|
|
1142
|
+
return resultData;
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
catch (fetchError) {
|
|
1146
|
+
logger.error('Failed to check for existing PR', fetchError);
|
|
1147
|
+
}
|
|
1148
|
+
// No existing PR found - check if we have commits to create one
|
|
1149
|
+
if (!result.success) {
|
|
1150
|
+
if (errorMatch) {
|
|
1151
|
+
throw new Error(`No commits to create PR. Branch '${headBranch}' is not ahead of '${targetBranch}'`);
|
|
1152
|
+
}
|
|
1153
|
+
throw new Error('Failed to prepare pull request');
|
|
1154
|
+
}
|
|
1155
|
+
// Create new PR
|
|
1156
|
+
const prTitle = title || `Merge ${headBranch} into ${targetBranch}`;
|
|
1157
|
+
const prBody = body || `This pull request merges changes from ${headBranch} into ${targetBranch}.\n\nCreated by AI-Playgrounds`;
|
|
1158
|
+
try {
|
|
1159
|
+
const prResponse = await octokit.rest.pulls.create({
|
|
1160
|
+
owner: gitOrgName,
|
|
1161
|
+
repo: repoName,
|
|
1162
|
+
title: prTitle,
|
|
1163
|
+
body: prBody,
|
|
1164
|
+
head: headBranch,
|
|
1165
|
+
base: targetBranch
|
|
1166
|
+
});
|
|
1167
|
+
const prUrl = prResponse.data.html_url;
|
|
1168
|
+
const prNumber = prResponse.data.number;
|
|
1169
|
+
logger.success('Pull request created successfully', {
|
|
1097
1170
|
repoName,
|
|
1098
|
-
prNumber
|
|
1099
|
-
prUrl
|
|
1171
|
+
prNumber,
|
|
1172
|
+
prUrl
|
|
1100
1173
|
});
|
|
1174
|
+
// taskInfo.status = TaskStatus.Completed;
|
|
1175
|
+
// taskInfo.nonRunningSince = new Date();
|
|
1176
|
+
// //updating the task status in the db
|
|
1177
|
+
// let dbService = await getDBService();
|
|
1178
|
+
// let taskHanlder = dbService.getRepository<Task>(ds.UserInfo.get(userId as any)?.dbName, CollectionNames.TASKS);
|
|
1179
|
+
// taskHanlder.updateOne({
|
|
1180
|
+
// taskId: taskId
|
|
1181
|
+
// },
|
|
1182
|
+
// {
|
|
1183
|
+
// "$set": {
|
|
1184
|
+
// status: TaskStatus.Completed
|
|
1185
|
+
// }
|
|
1186
|
+
// });
|
|
1101
1187
|
const resultData = {
|
|
1102
1188
|
repoName,
|
|
1103
1189
|
headBranch,
|
|
1104
1190
|
targetBranch,
|
|
1105
|
-
prNumber
|
|
1106
|
-
prUrl
|
|
1107
|
-
prTitle
|
|
1191
|
+
prNumber,
|
|
1192
|
+
prUrl,
|
|
1193
|
+
prTitle,
|
|
1108
1194
|
commitsAhead,
|
|
1109
|
-
createdAt:
|
|
1110
|
-
alreadyExists: true
|
|
1195
|
+
createdAt: new Date().toISOString()
|
|
1111
1196
|
};
|
|
1112
|
-
// Emit socket event to refresh PR status on client
|
|
1113
|
-
if (socket) {
|
|
1114
|
-
|
|
1115
|
-
}
|
|
1116
|
-
//
|
|
1117
|
-
//
|
|
1197
|
+
// // Emit socket event to refresh PR status on client
|
|
1198
|
+
// if (socket) {
|
|
1199
|
+
// socket.emit('refresh_pr_status', { repoName, prNumber });
|
|
1200
|
+
// }
|
|
1201
|
+
// // Update database asynchronously (non-blocking)
|
|
1202
|
+
// this.updatePRMetadata(userId, resultData);
|
|
1118
1203
|
return resultData;
|
|
1119
1204
|
}
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
// Create new PR
|
|
1132
|
-
const prTitle = title || `Merge ${headBranch} into ${targetBranch}`;
|
|
1133
|
-
const prBody = body || `This pull request merges changes from ${headBranch} into ${targetBranch}.\n\nCreated by AI-Playgrounds`;
|
|
1134
|
-
try {
|
|
1135
|
-
const prResponse = await octokit.rest.pulls.create({
|
|
1136
|
-
owner: gitOrgName,
|
|
1137
|
-
repo: repoName,
|
|
1138
|
-
title: prTitle,
|
|
1139
|
-
body: prBody,
|
|
1140
|
-
head: headBranch,
|
|
1141
|
-
base: targetBranch
|
|
1142
|
-
});
|
|
1143
|
-
const prUrl = prResponse.data.html_url;
|
|
1144
|
-
const prNumber = prResponse.data.number;
|
|
1145
|
-
logger.success('Pull request created successfully', {
|
|
1146
|
-
repoName,
|
|
1147
|
-
prNumber,
|
|
1148
|
-
prUrl
|
|
1149
|
-
});
|
|
1150
|
-
// taskInfo.status = TaskStatus.Completed;
|
|
1151
|
-
// taskInfo.nonRunningSince = new Date();
|
|
1152
|
-
// //updating the task status in the db
|
|
1153
|
-
// let dbService = await getDBService();
|
|
1154
|
-
// let taskHanlder = dbService.getRepository<Task>(ds.UserInfo.get(userId as any)?.dbName, CollectionNames.TASKS);
|
|
1155
|
-
// taskHanlder.updateOne({
|
|
1156
|
-
// taskId: taskId
|
|
1157
|
-
// },
|
|
1158
|
-
// {
|
|
1159
|
-
// "$set": {
|
|
1160
|
-
// status: TaskStatus.Completed
|
|
1161
|
-
// }
|
|
1162
|
-
// });
|
|
1163
|
-
const resultData = {
|
|
1164
|
-
repoName,
|
|
1165
|
-
headBranch,
|
|
1166
|
-
targetBranch,
|
|
1167
|
-
prNumber,
|
|
1168
|
-
prUrl,
|
|
1169
|
-
prTitle,
|
|
1170
|
-
commitsAhead,
|
|
1171
|
-
createdAt: new Date().toISOString()
|
|
1172
|
-
};
|
|
1173
|
-
// // Emit socket event to refresh PR status on client
|
|
1174
|
-
// if (socket) {
|
|
1175
|
-
// socket.emit('refresh_pr_status', { repoName, prNumber });
|
|
1176
|
-
// }
|
|
1177
|
-
// // Update database asynchronously (non-blocking)
|
|
1178
|
-
// this.updatePRMetadata(userId, resultData);
|
|
1179
|
-
return resultData;
|
|
1180
|
-
}
|
|
1181
|
-
catch (prError) {
|
|
1182
|
-
// Check if PR already exists
|
|
1183
|
-
if (prError.status === 422) {
|
|
1184
|
-
// Fetch existing PR
|
|
1185
|
-
try {
|
|
1186
|
-
const existingPRs = await octokit.rest.pulls.list({
|
|
1187
|
-
owner: gitOrgName,
|
|
1188
|
-
repo: repoName,
|
|
1189
|
-
head: `${gitOrgName}:${headBranch}`,
|
|
1190
|
-
base: targetBranch,
|
|
1191
|
-
state: 'open'
|
|
1192
|
-
});
|
|
1193
|
-
if (existingPRs.data.length > 0) {
|
|
1194
|
-
const existingPR = existingPRs.data[0];
|
|
1195
|
-
logger.info('Pull request already exists', {
|
|
1196
|
-
repoName,
|
|
1197
|
-
prNumber: existingPR.number,
|
|
1198
|
-
prUrl: existingPR.html_url
|
|
1205
|
+
catch (prError) {
|
|
1206
|
+
// Check if PR already exists
|
|
1207
|
+
if (prError.status === 422) {
|
|
1208
|
+
// Fetch existing PR
|
|
1209
|
+
try {
|
|
1210
|
+
const existingPRs = await octokit.rest.pulls.list({
|
|
1211
|
+
owner: gitOrgName,
|
|
1212
|
+
repo: repoName,
|
|
1213
|
+
head: `${gitOrgName}:${headBranch}`,
|
|
1214
|
+
base: targetBranch,
|
|
1215
|
+
state: 'open'
|
|
1199
1216
|
});
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1217
|
+
if (existingPRs.data.length > 0) {
|
|
1218
|
+
const existingPR = existingPRs.data[0];
|
|
1219
|
+
logger.info('Pull request already exists', {
|
|
1220
|
+
repoName,
|
|
1221
|
+
prNumber: existingPR.number,
|
|
1222
|
+
prUrl: existingPR.html_url
|
|
1223
|
+
});
|
|
1224
|
+
const resultData = {
|
|
1225
|
+
repoName,
|
|
1226
|
+
headBranch,
|
|
1227
|
+
targetBranch,
|
|
1228
|
+
prNumber: existingPR.number,
|
|
1229
|
+
prUrl: existingPR.html_url,
|
|
1230
|
+
prTitle: existingPR.title,
|
|
1231
|
+
commitsAhead,
|
|
1232
|
+
createdAt: existingPR.created_at,
|
|
1233
|
+
alreadyExists: true
|
|
1234
|
+
};
|
|
1235
|
+
// Emit socket event to refresh PR status on client
|
|
1236
|
+
if (socket) {
|
|
1237
|
+
socket.emit('refresh_pr_status', { repoName, prNumber: existingPR.number });
|
|
1238
|
+
}
|
|
1239
|
+
// Update database asynchronously (non-blocking)
|
|
1240
|
+
// this.updatePRMetadata(userId, resultData);
|
|
1241
|
+
return resultData;
|
|
1214
1242
|
}
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1243
|
+
}
|
|
1244
|
+
catch (fetchError) {
|
|
1245
|
+
logger.error('Failed to fetch existing PR', fetchError);
|
|
1218
1246
|
}
|
|
1219
1247
|
}
|
|
1220
|
-
|
|
1221
|
-
logger.error('Failed to fetch existing PR', fetchError);
|
|
1222
|
-
}
|
|
1248
|
+
throw prError;
|
|
1223
1249
|
}
|
|
1224
|
-
throw prError;
|
|
1225
1250
|
}
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
}
|
|
1251
|
+
finally {
|
|
1252
|
+
}
|
|
1253
|
+
}); // end withRepoLock
|
|
1229
1254
|
}
|
|
1230
1255
|
/**
|
|
1231
1256
|
* Check if pull request exists
|
|
@@ -1234,35 +1259,37 @@ class GithubOperationsService {
|
|
|
1234
1259
|
const repoPath = `${tool_server_1.folderPath}/${repoName}`;
|
|
1235
1260
|
const installationToken = await fetchInstallationToken();
|
|
1236
1261
|
const gitOrgName = await getGithubOrganizationName();
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1262
|
+
return withRepoLock(repoName, async () => {
|
|
1263
|
+
try {
|
|
1264
|
+
const trimmedSourceBranch = (sourceBranch || '').trim();
|
|
1265
|
+
const trimmedTargetBranch = (targetBranch || '').trim();
|
|
1266
|
+
if (!trimmedSourceBranch || !trimmedTargetBranch) {
|
|
1267
|
+
throw new Error('Both sourceBranch and targetBranch are required');
|
|
1268
|
+
}
|
|
1269
|
+
if (trimmedSourceBranch === trimmedTargetBranch) {
|
|
1270
|
+
throw new Error('sourceBranch and targetBranch cannot be the same');
|
|
1271
|
+
}
|
|
1272
|
+
logger.info('Starting branch merge operation', {
|
|
1273
|
+
repoName,
|
|
1274
|
+
sourceBranch: trimmedSourceBranch,
|
|
1275
|
+
targetBranch: trimmedTargetBranch,
|
|
1276
|
+
pushToRemote
|
|
1277
|
+
});
|
|
1278
|
+
let result = await executeShell(`cd ${repoPath} && rm -f .git/config.lock && ` +
|
|
1279
|
+
`git remote set-url origin https://x-access-token:${installationToken}@github.com/${gitOrgName}/${repoName}.git`);
|
|
1280
|
+
if (!result.success) {
|
|
1281
|
+
throw new Error(`Failed to set authenticated remote URL: ${result.error || result.output || 'Unknown error'}`);
|
|
1282
|
+
}
|
|
1283
|
+
const fetchResult = await executeShell(`cd ${repoPath} && sudo git fetch origin --prune`);
|
|
1284
|
+
if (!fetchResult.success) {
|
|
1285
|
+
throw new Error(`Failed to fetch remote branches: ${fetchResult.error || fetchResult.output || 'Unknown error'}`);
|
|
1286
|
+
}
|
|
1287
|
+
const resolvedSource = await this.resolveBranchReference(repoPath, repoName, trimmedSourceBranch);
|
|
1288
|
+
const resolvedTarget = await this.resolveBranchReference(repoPath, repoName, trimmedTargetBranch);
|
|
1289
|
+
const targetLocalBranch = resolvedTarget.displayBranch;
|
|
1290
|
+
const safeTargetLocalBranch = this.escapeSingleQuotedShell(targetLocalBranch);
|
|
1291
|
+
const safeSourceRef = this.escapeSingleQuotedShell(resolvedSource.resolvedRef);
|
|
1292
|
+
const prepareBranchCommand = `
|
|
1266
1293
|
cd ${repoPath} &&
|
|
1267
1294
|
TARGET_LOCAL='${safeTargetLocalBranch}' &&
|
|
1268
1295
|
if sudo git show-ref --verify --quiet "refs/heads/$TARGET_LOCAL"; then
|
|
@@ -1277,174 +1304,178 @@ class GithubOperationsService {
|
|
|
1277
1304
|
sudo git pull --ff-only origin "$TARGET_LOCAL" 2>/dev/null || true;
|
|
1278
1305
|
fi
|
|
1279
1306
|
`.replace(/\n\s+/g, ' ');
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
}
|
|
1284
|
-
const mergeCommand = `cd ${repoPath} && SOURCE_REF='${safeSourceRef}' && sudo git merge --no-ff --no-edit "$SOURCE_REF"`;
|
|
1285
|
-
const mergeResult = await executeShell(mergeCommand);
|
|
1286
|
-
if (!mergeResult.success) {
|
|
1287
|
-
const conflictResult = await executeShell(`cd ${repoPath} && sudo git diff --name-only --diff-filter=U`);
|
|
1288
|
-
const conflictFiles = (conflictResult.output || '')
|
|
1289
|
-
.split('\n')
|
|
1290
|
-
.map((line) => line.trim())
|
|
1291
|
-
.filter((line) => line.length > 0);
|
|
1292
|
-
if (conflictFiles.length > 0) {
|
|
1293
|
-
logger.warn('Merge completed with conflicts', {
|
|
1294
|
-
repoName,
|
|
1295
|
-
sourceBranch: resolvedSource.displayBranch,
|
|
1296
|
-
targetBranch: targetLocalBranch,
|
|
1297
|
-
conflictFiles
|
|
1298
|
-
});
|
|
1299
|
-
return {
|
|
1300
|
-
repoName,
|
|
1301
|
-
sourceBranch: resolvedSource.displayBranch,
|
|
1302
|
-
targetBranch: targetLocalBranch,
|
|
1303
|
-
merged: false,
|
|
1304
|
-
hasConflicts: true,
|
|
1305
|
-
conflictFiles,
|
|
1306
|
-
currentBranch: targetLocalBranch,
|
|
1307
|
-
pushedToRemote: false,
|
|
1308
|
-
mergedAt: new Date().toISOString(),
|
|
1309
|
-
message: `Merge has conflicts. Resolve conflicts in target branch '${targetLocalBranch}'.`
|
|
1310
|
-
};
|
|
1307
|
+
result = await executeShell(prepareBranchCommand);
|
|
1308
|
+
if (!result.success) {
|
|
1309
|
+
throw new Error(`Failed to prepare target branch for merge: ${result.error || result.output || 'Unknown error'}`);
|
|
1311
1310
|
}
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1311
|
+
const mergeCommand = `cd ${repoPath} && SOURCE_REF='${safeSourceRef}' && sudo git merge --no-ff --no-edit "$SOURCE_REF"`;
|
|
1312
|
+
const mergeResult = await executeShell(mergeCommand);
|
|
1313
|
+
if (!mergeResult.success) {
|
|
1314
|
+
const conflictResult = await executeShell(`cd ${repoPath} && sudo git diff --name-only --diff-filter=U`);
|
|
1315
|
+
const conflictFiles = (conflictResult.output || '')
|
|
1316
|
+
.split('\n')
|
|
1317
|
+
.map((line) => line.trim())
|
|
1318
|
+
.filter((line) => line.length > 0);
|
|
1319
|
+
if (conflictFiles.length > 0) {
|
|
1320
|
+
logger.warn('Merge completed with conflicts', {
|
|
1321
|
+
repoName,
|
|
1322
|
+
sourceBranch: resolvedSource.displayBranch,
|
|
1323
|
+
targetBranch: targetLocalBranch,
|
|
1324
|
+
conflictFiles
|
|
1325
|
+
});
|
|
1326
|
+
return {
|
|
1327
|
+
repoName,
|
|
1328
|
+
sourceBranch: resolvedSource.displayBranch,
|
|
1329
|
+
targetBranch: targetLocalBranch,
|
|
1330
|
+
merged: false,
|
|
1331
|
+
hasConflicts: true,
|
|
1332
|
+
conflictFiles,
|
|
1333
|
+
currentBranch: targetLocalBranch,
|
|
1334
|
+
pushedToRemote: false,
|
|
1335
|
+
mergedAt: new Date().toISOString(),
|
|
1336
|
+
message: `Merge has conflicts. Resolve conflicts in target branch '${targetLocalBranch}'.`
|
|
1337
|
+
};
|
|
1338
|
+
}
|
|
1339
|
+
throw new Error(`Failed to merge branches: ${mergeResult.error || mergeResult.output || 'Unknown error'}`);
|
|
1328
1340
|
}
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
conflictFiles: [],
|
|
1348
|
-
currentBranch,
|
|
1349
|
-
commitHash,
|
|
1350
|
-
pushedToRemote,
|
|
1351
|
-
mergedAt: new Date().toISOString(),
|
|
1352
|
-
message: pushedToRemote
|
|
1353
|
-
? `Merged '${resolvedSource.displayBranch}' into '${targetLocalBranch}' and pushed to remote.`
|
|
1354
|
-
: `Merged '${resolvedSource.displayBranch}' into '${targetLocalBranch}' locally.`
|
|
1355
|
-
};
|
|
1356
|
-
}
|
|
1357
|
-
finally {
|
|
1358
|
-
try {
|
|
1359
|
-
await executeShell(`cd ${repoPath} && sudo git remote set-url origin https://github.com/${gitOrgName}/${repoName}.git`);
|
|
1360
|
-
}
|
|
1361
|
-
catch (restoreError) {
|
|
1362
|
-
logger.error('Failed to restore unauthenticated remote URL after merge operation', {
|
|
1341
|
+
const summaryResult = await executeShell(`cd ${repoPath} && COMMIT_HASH=$(sudo git rev-parse HEAD) && CURRENT_BRANCH=$(sudo git rev-parse --abbrev-ref HEAD) && echo "COMMIT_HASH:$COMMIT_HASH" && echo "CURRENT_BRANCH:$CURRENT_BRANCH"`);
|
|
1342
|
+
if (!summaryResult.success) {
|
|
1343
|
+
throw new Error(`Merge succeeded but failed to fetch merge summary: ${summaryResult.error || summaryResult.output || 'Unknown error'}`);
|
|
1344
|
+
}
|
|
1345
|
+
const summaryOutput = summaryResult.output || '';
|
|
1346
|
+
const commitHashMatch = summaryOutput.match(/COMMIT_HASH:([^\n]+)/);
|
|
1347
|
+
const currentBranchMatch = summaryOutput.match(/CURRENT_BRANCH:([^\n]+)/);
|
|
1348
|
+
const commitHash = commitHashMatch ? commitHashMatch[1].trim() : '';
|
|
1349
|
+
const currentBranch = currentBranchMatch ? currentBranchMatch[1].trim() : targetLocalBranch;
|
|
1350
|
+
let pushedToRemote = false;
|
|
1351
|
+
if (pushToRemote) {
|
|
1352
|
+
const pushResult = await executeShell(`cd ${repoPath} && sudo git push origin '${safeTargetLocalBranch}'`);
|
|
1353
|
+
if (!pushResult.success) {
|
|
1354
|
+
throw new Error(`Merge succeeded but push failed: ${pushResult.error || pushResult.output || 'Unknown error'}`);
|
|
1355
|
+
}
|
|
1356
|
+
pushedToRemote = true;
|
|
1357
|
+
}
|
|
1358
|
+
logger.success('Branch merge completed successfully', {
|
|
1363
1359
|
repoName,
|
|
1364
|
-
|
|
1360
|
+
sourceBranch: resolvedSource.displayBranch,
|
|
1361
|
+
targetBranch: targetLocalBranch,
|
|
1362
|
+
commitHash,
|
|
1363
|
+
pushedToRemote
|
|
1365
1364
|
});
|
|
1365
|
+
if (socket) {
|
|
1366
|
+
socket.emit('refresh_repo', { repoName });
|
|
1367
|
+
}
|
|
1368
|
+
return {
|
|
1369
|
+
repoName,
|
|
1370
|
+
sourceBranch: resolvedSource.displayBranch,
|
|
1371
|
+
targetBranch: targetLocalBranch,
|
|
1372
|
+
merged: true,
|
|
1373
|
+
hasConflicts: false,
|
|
1374
|
+
conflictFiles: [],
|
|
1375
|
+
currentBranch,
|
|
1376
|
+
commitHash,
|
|
1377
|
+
pushedToRemote,
|
|
1378
|
+
mergedAt: new Date().toISOString(),
|
|
1379
|
+
message: pushedToRemote
|
|
1380
|
+
? `Merged '${resolvedSource.displayBranch}' into '${targetLocalBranch}' and pushed to remote.`
|
|
1381
|
+
: `Merged '${resolvedSource.displayBranch}' into '${targetLocalBranch}' locally.`
|
|
1382
|
+
};
|
|
1366
1383
|
}
|
|
1367
|
-
|
|
1384
|
+
finally {
|
|
1385
|
+
try {
|
|
1386
|
+
await executeShell(`cd ${repoPath} && git remote set-url origin https://github.com/${gitOrgName}/${repoName}.git`);
|
|
1387
|
+
}
|
|
1388
|
+
catch (restoreError) {
|
|
1389
|
+
logger.error('Failed to restore unauthenticated remote URL after merge operation', {
|
|
1390
|
+
repoName,
|
|
1391
|
+
error: restoreError instanceof Error ? restoreError.message : String(restoreError)
|
|
1392
|
+
});
|
|
1393
|
+
}
|
|
1394
|
+
}
|
|
1395
|
+
}); // end withRepoLock
|
|
1368
1396
|
}
|
|
1369
1397
|
async checkPullRequestExists(repoName, targetBranch) {
|
|
1370
1398
|
const repoPath = `${tool_server_1.folderPath}/${repoName}`;
|
|
1371
1399
|
const installationToken = await fetchInstallationToken();
|
|
1372
1400
|
const gitOrgName = await getGithubOrganizationName();
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1401
|
+
return withRepoLock(repoName, async () => {
|
|
1402
|
+
try {
|
|
1403
|
+
logger.info('Checking if PR exists', { repoName, targetBranch });
|
|
1404
|
+
// Get current branch and commits ahead
|
|
1405
|
+
const combinedCommand = `
|
|
1377
1406
|
cd ${repoPath} &&
|
|
1378
|
-
|
|
1407
|
+
rm -f .git/config.lock &&
|
|
1408
|
+
git remote set-url origin https://x-access-token:${installationToken}@github.com/${gitOrgName}/${repoName}.git &&
|
|
1379
1409
|
HEAD_BRANCH=$(sudo git rev-parse --abbrev-ref HEAD) &&
|
|
1380
1410
|
sudo git fetch origin --prune 2>/dev/null || true &&
|
|
1381
1411
|
sudo git fetch origin ${targetBranch} 2>/dev/null || true &&
|
|
1382
1412
|
COMMITS_AHEAD=$(sudo git rev-list --count origin/${targetBranch}..$HEAD_BRANCH 2>/dev/null || echo 0) &&
|
|
1383
|
-
|
|
1413
|
+
git remote set-url origin https://github.com/${gitOrgName}/${repoName}.git &&
|
|
1384
1414
|
echo "HEAD_BRANCH:$HEAD_BRANCH" &&
|
|
1385
1415
|
echo "COMMITS_AHEAD:$COMMITS_AHEAD"
|
|
1386
1416
|
`.replace(/\n\s+/g, ' ');
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1417
|
+
const result = await executeShell(combinedCommand);
|
|
1418
|
+
// Check for command execution failure
|
|
1419
|
+
if (!result.success) {
|
|
1420
|
+
logger.error('Failed to check PR status', {
|
|
1421
|
+
repoName,
|
|
1422
|
+
error: result.error,
|
|
1423
|
+
code: result.code,
|
|
1424
|
+
output: result.output
|
|
1425
|
+
});
|
|
1426
|
+
throw new Error(`Failed to check PR status: ${result.error}`);
|
|
1427
|
+
}
|
|
1428
|
+
const output = result.output || '';
|
|
1429
|
+
const headBranchMatch = output.match(/HEAD_BRANCH:([^\n]+)/);
|
|
1430
|
+
const commitsAheadMatch = output.match(/COMMITS_AHEAD:(\d+)/);
|
|
1431
|
+
const headBranch = headBranchMatch ? headBranchMatch[1].trim() : 'unknown';
|
|
1432
|
+
const commitsAhead = commitsAheadMatch ? parseInt(commitsAheadMatch[1]) : 0;
|
|
1433
|
+
logger.info('Branch info retrieved', { headBranch, commitsAhead });
|
|
1434
|
+
// Check for existing PR - using 'octokit' package (already installed)
|
|
1435
|
+
const { Octokit } = require('octokit');
|
|
1436
|
+
const octokit = new Octokit({
|
|
1437
|
+
auth: installationToken
|
|
1395
1438
|
});
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
const commitsAhead = commitsAheadMatch ? parseInt(commitsAheadMatch[1]) : 0;
|
|
1403
|
-
logger.info('Branch info retrieved', { headBranch, commitsAhead });
|
|
1404
|
-
// Check for existing PR - using 'octokit' package (already installed)
|
|
1405
|
-
const { Octokit } = require('octokit');
|
|
1406
|
-
const octokit = new Octokit({
|
|
1407
|
-
auth: installationToken
|
|
1408
|
-
});
|
|
1409
|
-
const existingPRs = await octokit.rest.pulls.list({
|
|
1410
|
-
owner: gitOrgName,
|
|
1411
|
-
repo: repoName,
|
|
1412
|
-
head: `${gitOrgName}:${headBranch}`,
|
|
1413
|
-
base: targetBranch,
|
|
1414
|
-
state: 'open'
|
|
1415
|
-
});
|
|
1416
|
-
if (existingPRs.data.length > 0) {
|
|
1417
|
-
const existingPR = existingPRs.data[0];
|
|
1418
|
-
logger.info('Pull request exists', {
|
|
1419
|
-
repoName,
|
|
1420
|
-
prNumber: existingPR.number,
|
|
1421
|
-
prUrl: existingPR.html_url
|
|
1439
|
+
const existingPRs = await octokit.rest.pulls.list({
|
|
1440
|
+
owner: gitOrgName,
|
|
1441
|
+
repo: repoName,
|
|
1442
|
+
head: `${gitOrgName}:${headBranch}`,
|
|
1443
|
+
base: targetBranch,
|
|
1444
|
+
state: 'open'
|
|
1422
1445
|
});
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1446
|
+
if (existingPRs.data.length > 0) {
|
|
1447
|
+
const existingPR = existingPRs.data[0];
|
|
1448
|
+
logger.info('Pull request exists', {
|
|
1449
|
+
repoName,
|
|
1450
|
+
prNumber: existingPR.number,
|
|
1451
|
+
prUrl: existingPR.html_url
|
|
1452
|
+
});
|
|
1453
|
+
return {
|
|
1454
|
+
repoName,
|
|
1455
|
+
headBranch,
|
|
1456
|
+
targetBranch,
|
|
1457
|
+
prExists: true,
|
|
1458
|
+
prNumber: existingPR.number,
|
|
1459
|
+
prUrl: existingPR.html_url,
|
|
1460
|
+
prTitle: existingPR.title,
|
|
1461
|
+
commitsAhead,
|
|
1462
|
+
createdAt: existingPR.created_at
|
|
1463
|
+
};
|
|
1464
|
+
}
|
|
1465
|
+
else {
|
|
1466
|
+
logger.info('No pull request found', { repoName, headBranch, targetBranch });
|
|
1467
|
+
return {
|
|
1468
|
+
repoName,
|
|
1469
|
+
headBranch,
|
|
1470
|
+
targetBranch,
|
|
1471
|
+
prExists: false,
|
|
1472
|
+
commitsAhead
|
|
1473
|
+
};
|
|
1474
|
+
}
|
|
1434
1475
|
}
|
|
1435
|
-
|
|
1436
|
-
logger.info('No pull request found', { repoName, headBranch, targetBranch });
|
|
1437
|
-
return {
|
|
1438
|
-
repoName,
|
|
1439
|
-
headBranch,
|
|
1440
|
-
targetBranch,
|
|
1441
|
-
prExists: false,
|
|
1442
|
-
commitsAhead
|
|
1443
|
-
};
|
|
1476
|
+
finally {
|
|
1444
1477
|
}
|
|
1445
|
-
}
|
|
1446
|
-
finally {
|
|
1447
|
-
}
|
|
1478
|
+
}); // end withRepoLock
|
|
1448
1479
|
}
|
|
1449
1480
|
/**
|
|
1450
1481
|
* Get commit list with commit IDs for a branch
|
|
@@ -1943,7 +1974,7 @@ class GithubOperationsService {
|
|
|
1943
1974
|
/**
|
|
1944
1975
|
* Helper method to get merge conflicts
|
|
1945
1976
|
*/
|
|
1946
|
-
async getMergeConflicts(
|
|
1977
|
+
async getMergeConflicts(repoName) {
|
|
1947
1978
|
return new Promise((resolve) => {
|
|
1948
1979
|
this.getMergeConflictsImplementation(repoName, tool_server_1.folderPath).then((mergeData) => {
|
|
1949
1980
|
resolve(mergeData);
|