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.
@@ -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
- try {
601
- logger.info('Getting repository origin/default branch', { repoName });
602
- // Set authenticated remote URL
603
- await executeShell(`cd ${repoPath} && sudo git remote set-url origin https://x-access-token:${installationToken}@github.com/${gitOrgName}/${repoName}.git`);
604
- // Fetch remote HEAD to find the default branch
605
- const result = await executeShell(`cd ${repoPath} && sudo git remote show origin | grep 'HEAD branch' | awk '{print $NF}'`);
606
- // Restore unauthenticated remote URL
607
- await executeShell(`cd ${repoPath} && sudo git remote set-url origin https://github.com/${gitOrgName}/${repoName}.git`);
608
- if (!result.success || !result.output.trim()) {
609
- throw new Error(`Failed to get origin branch for repo '${repoName}': ${result.error || result.output}`);
610
- }
611
- const originBranch = result.output.trim();
612
- logger.success('Repository origin branch retrieved', { repoName, originBranch });
613
- return {
614
- repoName,
615
- originBranch
616
- };
617
- }
618
- finally { }
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
- try {
640
- // Get current local branch name
641
- let result = await executeShell(`cd ${repoPath} && sudo git rev-parse --abbrev-ref HEAD`);
642
- if (!result.success) {
643
- throw new Error(`Failed to get current branch: ${result.error}`);
644
- }
645
- const currentBranch = result.output.trim();
646
- logger.info('starting git pull operation', { repoName, currentBranch });
647
- // Set remote url with token
648
- result = await executeShell(`cd ${repoPath} && sudo git remote set-url origin https://x-access-token:${installationToken}@github.com/${gitOrgName}/${repoName}.git`);
649
- if (!result.success) {
650
- throw new Error(`Failed to set remote URL: ${result.error}`);
651
- }
652
- result = await executeShell(`cd ${repoPath} && sudo git fetch origin`);
653
- if (!result.success) {
654
- throw new Error(`Failed to fetch from remote: ${result.error}`);
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
- result = await executeShell(`cd ${repoPath} && sudo git pull --no-rebase origin ${currentBranch}`);
657
- if (!result.success && result.code !== 128) {
658
- throw new Error(`Failed to pull branch ${currentBranch}: ${result.error}`);
682
+ finally {
659
683
  }
660
- await executeShell(`cd ${repoPath} && sudo git remote set-url origin https://github.com/${gitOrgName}/${repoName}.git`);
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
- try {
682
- logger.info('Starting git push operation', { repoName });
683
- // Combine all commands into a single SSH call
684
- const combinedCommand = `
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
- sudo git remote set-url origin https://x-access-token:${installationToken}@github.com/${gitOrgName}/${repoName}.git &&
706
+ git remote set-url origin https://x-access-token:${installationToken}@github.com/${gitOrgName}/${repoName}.git &&
693
707
  sudo git push origin $BRANCH &&
694
- sudo git remote set-url origin https://github.com/${gitOrgName}/${repoName}.git &&
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
- const result = await executeShell(combinedCommand);
701
- if (!result.success) {
702
- if (result.output && result.output.includes('ERROR:NO_COMMITS')) {
703
- throw new Error('No commits to push');
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
- throw new Error('Failed to push repository');
734
+ return {
735
+ repoName,
736
+ branchName: currentBranch,
737
+ commitHash,
738
+ commitsAhead,
739
+ pushedAt: new Date().toISOString()
740
+ };
706
741
  }
707
- // Parse output
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
- return {
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
- try {
739
- logger.info('Checking repository status', { repoName });
740
- // Combine all git commands into a single SSH call
741
- const combinedCommand = `
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
- sudo git remote set-url origin https://x-access-token:${installationToken}@github.com/${gitOrgName}/${repoName}.git &&
765
+ git remote set-url origin https://x-access-token:${installationToken}@github.com/${gitOrgName}/${repoName}.git &&
745
766
  sudo git fetch origin &&
746
- sudo git remote set-url origin https://github.com/${gitOrgName}/${repoName}.git &&
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
- const result = await executeShell(combinedCommand);
760
- if (!result.success) {
761
- throw new Error('Failed to check repository status');
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
- // Parse the output
764
- const output = result.output;
765
- const branchMatch = output.match(/BRANCH:([^\n]+)/);
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
- try {
1053
- logger.info('Creating pull request', { repoName, targetBranch });
1054
- // Combine commands into a single SSH call, with remote URL set/unset
1055
- const combinedCommand = `
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
- sudo git remote set-url origin https://x-access-token:${installationToken}@github.com/${gitOrgName}/${repoName}.git &&
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
- sudo git remote set-url origin https://github.com/${gitOrgName}/${repoName}.git;
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
- sudo git remote set-url origin https://github.com/${gitOrgName}/${repoName}.git
1092
+ git remote set-url origin https://github.com/${gitOrgName}/${repoName}.git
1069
1093
  `.replace(/\n\s+/g, ' ');
1070
- let result = await executeShell(combinedCommand);
1071
- const output = result.output || '';
1072
- const headBranchMatch = output.match(/HEAD_BRANCH:([^\n]+)/);
1073
- const commitsAheadMatch = output.match(/COMMITS_AHEAD:(\d+)/);
1074
- const errorMatch = output.match(/ERROR:NO_COMMITS:([^\n]+)/);
1075
- let headBranch = headBranchMatch ? headBranchMatch[1].trim() : 'unknown';
1076
- const commitsAhead = commitsAheadMatch ? parseInt(commitsAheadMatch[1]) : 0;
1077
- if (!result.success && errorMatch) {
1078
- headBranch = errorMatch[1].trim();
1079
- }
1080
- // using 'octokit' package (already installed, @octokit/rest is not installed)
1081
- const { Octokit } = require('octokit');
1082
- const octokit = new Octokit({
1083
- auth: installationToken
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 (existingPRs.data.length > 0) {
1095
- const existingPR = existingPRs.data[0];
1096
- logger.info('Pull request already exists', {
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: existingPR.number,
1099
- prUrl: existingPR.html_url
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: existingPR.number,
1106
- prUrl: existingPR.html_url,
1107
- prTitle: existingPR.title,
1191
+ prNumber,
1192
+ prUrl,
1193
+ prTitle,
1108
1194
  commitsAhead,
1109
- createdAt: existingPR.created_at,
1110
- alreadyExists: true
1195
+ createdAt: new Date().toISOString()
1111
1196
  };
1112
- // Emit socket event to refresh PR status on client
1113
- if (socket) {
1114
- socket.emit('refresh_pr_status', { repoName, prNumber: existingPR.number });
1115
- }
1116
- // the socket server our main server will automatically update the pr metadata in db when i recvs the result of the given tool.
1117
- // this.updatePRMetadata(userId, resultData);
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
- catch (fetchError) {
1122
- logger.error('Failed to check for existing PR', fetchError);
1123
- }
1124
- // No existing PR found - check if we have commits to create one
1125
- if (!result.success) {
1126
- if (errorMatch) {
1127
- throw new Error(`No commits to create PR. Branch '${headBranch}' is not ahead of '${targetBranch}'`);
1128
- }
1129
- throw new Error('Failed to prepare pull request');
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
- const resultData = {
1201
- repoName,
1202
- headBranch,
1203
- targetBranch,
1204
- prNumber: existingPR.number,
1205
- prUrl: existingPR.html_url,
1206
- prTitle: existingPR.title,
1207
- commitsAhead,
1208
- createdAt: existingPR.created_at,
1209
- alreadyExists: true
1210
- };
1211
- // Emit socket event to refresh PR status on client
1212
- if (socket) {
1213
- socket.emit('refresh_pr_status', { repoName, prNumber: existingPR.number });
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
- // Update database asynchronously (non-blocking)
1216
- // this.updatePRMetadata(userId, resultData);
1217
- return resultData;
1243
+ }
1244
+ catch (fetchError) {
1245
+ logger.error('Failed to fetch existing PR', fetchError);
1218
1246
  }
1219
1247
  }
1220
- catch (fetchError) {
1221
- logger.error('Failed to fetch existing PR', fetchError);
1222
- }
1248
+ throw prError;
1223
1249
  }
1224
- throw prError;
1225
1250
  }
1226
- }
1227
- finally {
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
- try {
1238
- const trimmedSourceBranch = (sourceBranch || '').trim();
1239
- const trimmedTargetBranch = (targetBranch || '').trim();
1240
- if (!trimmedSourceBranch || !trimmedTargetBranch) {
1241
- throw new Error('Both sourceBranch and targetBranch are required');
1242
- }
1243
- if (trimmedSourceBranch === trimmedTargetBranch) {
1244
- throw new Error('sourceBranch and targetBranch cannot be the same');
1245
- }
1246
- logger.info('Starting branch merge operation', {
1247
- repoName,
1248
- sourceBranch: trimmedSourceBranch,
1249
- targetBranch: trimmedTargetBranch,
1250
- pushToRemote
1251
- });
1252
- let result = await executeShell(`cd ${repoPath} && sudo git remote set-url origin https://x-access-token:${installationToken}@github.com/${gitOrgName}/${repoName}.git`);
1253
- if (!result.success) {
1254
- throw new Error(`Failed to set authenticated remote URL: ${result.error || result.output || 'Unknown error'}`);
1255
- }
1256
- const fetchResult = await executeShell(`cd ${repoPath} && sudo git fetch origin --prune`);
1257
- if (!fetchResult.success) {
1258
- throw new Error(`Failed to fetch remote branches: ${fetchResult.error || fetchResult.output || 'Unknown error'}`);
1259
- }
1260
- const resolvedSource = await this.resolveBranchReference(repoPath, repoName, trimmedSourceBranch);
1261
- const resolvedTarget = await this.resolveBranchReference(repoPath, repoName, trimmedTargetBranch);
1262
- const targetLocalBranch = resolvedTarget.displayBranch;
1263
- const safeTargetLocalBranch = this.escapeSingleQuotedShell(targetLocalBranch);
1264
- const safeSourceRef = this.escapeSingleQuotedShell(resolvedSource.resolvedRef);
1265
- const prepareBranchCommand = `
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
- result = await executeShell(prepareBranchCommand);
1281
- if (!result.success) {
1282
- throw new Error(`Failed to prepare target branch for merge: ${result.error || result.output || 'Unknown error'}`);
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
- throw new Error(`Failed to merge branches: ${mergeResult.error || mergeResult.output || 'Unknown error'}`);
1313
- }
1314
- 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"`);
1315
- if (!summaryResult.success) {
1316
- throw new Error(`Merge succeeded but failed to fetch merge summary: ${summaryResult.error || summaryResult.output || 'Unknown error'}`);
1317
- }
1318
- const summaryOutput = summaryResult.output || '';
1319
- const commitHashMatch = summaryOutput.match(/COMMIT_HASH:([^\n]+)/);
1320
- const currentBranchMatch = summaryOutput.match(/CURRENT_BRANCH:([^\n]+)/);
1321
- const commitHash = commitHashMatch ? commitHashMatch[1].trim() : '';
1322
- const currentBranch = currentBranchMatch ? currentBranchMatch[1].trim() : targetLocalBranch;
1323
- let pushedToRemote = false;
1324
- if (pushToRemote) {
1325
- const pushResult = await executeShell(`cd ${repoPath} && sudo git push origin '${safeTargetLocalBranch}'`);
1326
- if (!pushResult.success) {
1327
- throw new Error(`Merge succeeded but push failed: ${pushResult.error || pushResult.output || 'Unknown error'}`);
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
- pushedToRemote = true;
1330
- }
1331
- logger.success('Branch merge completed successfully', {
1332
- repoName,
1333
- sourceBranch: resolvedSource.displayBranch,
1334
- targetBranch: targetLocalBranch,
1335
- commitHash,
1336
- pushedToRemote
1337
- });
1338
- if (socket) {
1339
- socket.emit('refresh_repo', { repoName });
1340
- }
1341
- return {
1342
- repoName,
1343
- sourceBranch: resolvedSource.displayBranch,
1344
- targetBranch: targetLocalBranch,
1345
- merged: true,
1346
- hasConflicts: false,
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
- error: restoreError instanceof Error ? restoreError.message : String(restoreError)
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
- try {
1374
- logger.info('Checking if PR exists', { repoName, targetBranch });
1375
- // Get current branch and commits ahead
1376
- const combinedCommand = `
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
- sudo git remote set-url origin https://x-access-token:${installationToken}@github.com/${gitOrgName}/${repoName}.git &&
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
- sudo git remote set-url origin https://github.com/${gitOrgName}/${repoName}.git &&
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
- const result = await executeShell(combinedCommand);
1388
- // Check for command execution failure
1389
- if (!result.success) {
1390
- logger.error('Failed to check PR status', {
1391
- repoName,
1392
- error: result.error,
1393
- code: result.code,
1394
- output: result.output
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
- throw new Error(`Failed to check PR status: ${result.error}`);
1397
- }
1398
- const output = result.output || '';
1399
- const headBranchMatch = output.match(/HEAD_BRANCH:([^\n]+)/);
1400
- const commitsAheadMatch = output.match(/COMMITS_AHEAD:(\d+)/);
1401
- const headBranch = headBranchMatch ? headBranchMatch[1].trim() : 'unknown';
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
- return {
1424
- repoName,
1425
- headBranch,
1426
- targetBranch,
1427
- prExists: true,
1428
- prNumber: existingPR.number,
1429
- prUrl: existingPR.html_url,
1430
- prTitle: existingPR.title,
1431
- commitsAhead,
1432
- createdAt: existingPR.created_at
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
- else {
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(socket, repoName) {
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);