phantomx-tool-client 1.1.4 → 1.1.6

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(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,17 +750,15 @@ class GithubOperationsService {
735
750
  const repoPath = `${tool_server_1.folderPath}/${repoName}`;
736
751
  const installationToken = await fetchInstallationToken();
737
752
  const gitOrgName = await getGithubOrganizationName();
738
- // const repoPath = 'mnt/f/PhantomX-workspace/93943752-2576-44ee-9b57-4b6fb3130fe4/AI_CODER_REMOTE'
739
- // const installationToken = "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";
740
- // const gitOrgName = "AI-PLAYGROUNDS";
741
- try {
742
- logger.info('Checking repository status', { repoName });
743
- // Combine all git commands into a single SSH call
744
- // Remove any stale .git/config.lock before touching remote config
745
- // (a crashed/interrupted previous run can leave this lock behind).
746
- // Also avoid sudo for git remote set-url so the lock file is never
747
- // created as root-owned, which would block subsequent cleanup.
748
- 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 = `
749
762
  cd ${repoPath} &&
750
763
  rm -f .git/config.lock &&
751
764
  BRANCH=$(sudo git rev-parse --abbrev-ref HEAD) &&
@@ -764,54 +777,55 @@ class GithubOperationsService {
764
777
  echo "CURRENT:$CURRENT_HASH" &&
765
778
  echo "REMOTE:$REMOTE_HASH"
766
779
  `.replace(/\n\s+/g, ' ');
767
- const result = await executeShell(combinedCommand);
768
- if (!result.success) {
769
- throw new Error('Failed to check repository status: ' + result.error);
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
+ };
770
825
  }
771
- // Parse the output
772
- const output = result.output;
773
- const branchMatch = output.match(/BRANCH:([^\n]+)/);
774
- const behindMatch = output.match(/BEHIND:(\d+)/);
775
- const aheadMatch = output.match(/AHEAD:(\d+)/);
776
- const statusMatch = output.match(/STATUS:([\s\S]*?)(?=CURRENT:|$)/);
777
- const currentMatch = output.match(/CURRENT:([a-f0-9]+)/);
778
- const remoteMatch = output.match(/REMOTE:([a-f0-9]+|unknown)/);
779
- const currentBranch = branchMatch ? branchMatch[1].trim() : 'unknown';
780
- const commitsBehind = behindMatch ? parseInt(behindMatch[1]) : 0;
781
- const commitsAhead = aheadMatch ? parseInt(aheadMatch[1]) : 0;
782
- const statusOutput = statusMatch ? statusMatch[1].trim() : '';
783
- const hasUncommittedChanges = statusOutput.length > 0;
784
- const currentCommitHash = currentMatch ? currentMatch[1].trim() : 'unknown';
785
- const remoteCommitHash = remoteMatch ? remoteMatch[1].trim() : 'unknown';
786
- const isUpToDate = commitsBehind === 0 && commitsAhead === 0;
787
- const needsPull = commitsBehind > 0;
788
- const needsPush = commitsAhead > 0;
789
- logger.info('Repository status checked', {
790
- repoName,
791
- commitsBehind,
792
- commitsAhead,
793
- hasUncommittedChanges,
794
- isUpToDate
795
- });
796
- // Check for merge conflicts
797
- const mergeConflicts = await this.getMergeConflicts(repoName);
798
- return {
799
- repoName,
800
- branchName: currentBranch,
801
- currentCommitHash,
802
- remoteCommitHash,
803
- commitsBehind,
804
- commitsAhead,
805
- hasUncommittedChanges,
806
- isUpToDate,
807
- needsPull,
808
- needsPush,
809
- mergeConflicts,
810
- status: isUpToDate ? 'up-to-date' : (needsPull && needsPush ? 'diverged' : needsPull ? 'behind' : 'ahead')
811
- };
812
- }
813
- finally {
814
- }
826
+ finally {
827
+ }
828
+ }); // end withRepoLock
815
829
  }
816
830
  /**
817
831
  * Get repository history
@@ -1057,183 +1071,186 @@ class GithubOperationsService {
1057
1071
  const repoPath = `${tool_server_1.folderPath}/${repoName}`;
1058
1072
  const installationToken = await fetchInstallationToken();
1059
1073
  const gitOrgName = await getGithubOrganizationName();
1060
- try {
1061
- logger.info('Creating pull request', { repoName, targetBranch });
1062
- // Combine commands into a single SSH call, with remote URL set/unset
1063
- 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 = `
1064
1079
  cd ${repoPath} &&
1065
- 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 &&
1066
1082
  HEAD_BRANCH=$(sudo git rev-parse --abbrev-ref HEAD) &&
1067
1083
  sudo git fetch origin ${targetBranch} 2>/dev/null &&
1068
1084
  COMMITS_AHEAD=$(sudo git rev-list --count origin/${targetBranch}..$HEAD_BRANCH 2>/dev/null || echo 0) &&
1069
1085
  if [ "$COMMITS_AHEAD" = "0" ]; then
1070
1086
  echo "ERROR:NO_COMMITS:$HEAD_BRANCH";
1071
- sudo git remote set-url origin https://github.com/${gitOrgName}/${repoName}.git;
1087
+ git remote set-url origin https://github.com/${gitOrgName}/${repoName}.git;
1072
1088
  exit 1;
1073
1089
  fi &&
1074
1090
  echo "HEAD_BRANCH:$HEAD_BRANCH" &&
1075
1091
  echo "COMMITS_AHEAD:$COMMITS_AHEAD" &&
1076
- sudo git remote set-url origin https://github.com/${gitOrgName}/${repoName}.git
1092
+ git remote set-url origin https://github.com/${gitOrgName}/${repoName}.git
1077
1093
  `.replace(/\n\s+/g, ' ');
1078
- let result = await executeShell(combinedCommand);
1079
- const output = result.output || '';
1080
- const headBranchMatch = output.match(/HEAD_BRANCH:([^\n]+)/);
1081
- const commitsAheadMatch = output.match(/COMMITS_AHEAD:(\d+)/);
1082
- const errorMatch = output.match(/ERROR:NO_COMMITS:([^\n]+)/);
1083
- let headBranch = headBranchMatch ? headBranchMatch[1].trim() : 'unknown';
1084
- const commitsAhead = commitsAheadMatch ? parseInt(commitsAheadMatch[1]) : 0;
1085
- if (!result.success && errorMatch) {
1086
- headBranch = errorMatch[1].trim();
1087
- }
1088
- // using 'octokit' package (already installed, @octokit/rest is not installed)
1089
- const { Octokit } = require('octokit');
1090
- const octokit = new Octokit({
1091
- auth: installationToken
1092
- });
1093
- // Always check for existing PR first even if there are no new commits
1094
- try {
1095
- const existingPRs = await octokit.rest.pulls.list({
1096
- owner: gitOrgName,
1097
- repo: repoName,
1098
- head: `${gitOrgName}:${headBranch}`,
1099
- base: targetBranch,
1100
- 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
1101
1108
  });
1102
- if (existingPRs.data.length > 0) {
1103
- const existingPR = existingPRs.data[0];
1104
- 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', {
1105
1170
  repoName,
1106
- prNumber: existingPR.number,
1107
- prUrl: existingPR.html_url
1171
+ prNumber,
1172
+ prUrl
1108
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
+ // });
1109
1187
  const resultData = {
1110
1188
  repoName,
1111
1189
  headBranch,
1112
1190
  targetBranch,
1113
- prNumber: existingPR.number,
1114
- prUrl: existingPR.html_url,
1115
- prTitle: existingPR.title,
1191
+ prNumber,
1192
+ prUrl,
1193
+ prTitle,
1116
1194
  commitsAhead,
1117
- createdAt: existingPR.created_at,
1118
- alreadyExists: true
1195
+ createdAt: new Date().toISOString()
1119
1196
  };
1120
- // Emit socket event to refresh PR status on client
1121
- if (socket) {
1122
- socket.emit('refresh_pr_status', { repoName, prNumber: existingPR.number });
1123
- }
1124
- // the socket server our main server will automatically update the pr metadata in db when i recvs the result of the given tool.
1125
- // 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);
1126
1203
  return resultData;
1127
1204
  }
1128
- }
1129
- catch (fetchError) {
1130
- logger.error('Failed to check for existing PR', fetchError);
1131
- }
1132
- // No existing PR found - check if we have commits to create one
1133
- if (!result.success) {
1134
- if (errorMatch) {
1135
- throw new Error(`No commits to create PR. Branch '${headBranch}' is not ahead of '${targetBranch}'`);
1136
- }
1137
- throw new Error('Failed to prepare pull request');
1138
- }
1139
- // Create new PR
1140
- const prTitle = title || `Merge ${headBranch} into ${targetBranch}`;
1141
- const prBody = body || `This pull request merges changes from ${headBranch} into ${targetBranch}.\n\nCreated by AI-Playgrounds`;
1142
- try {
1143
- const prResponse = await octokit.rest.pulls.create({
1144
- owner: gitOrgName,
1145
- repo: repoName,
1146
- title: prTitle,
1147
- body: prBody,
1148
- head: headBranch,
1149
- base: targetBranch
1150
- });
1151
- const prUrl = prResponse.data.html_url;
1152
- const prNumber = prResponse.data.number;
1153
- logger.success('Pull request created successfully', {
1154
- repoName,
1155
- prNumber,
1156
- prUrl
1157
- });
1158
- // taskInfo.status = TaskStatus.Completed;
1159
- // taskInfo.nonRunningSince = new Date();
1160
- // //updating the task status in the db
1161
- // let dbService = await getDBService();
1162
- // let taskHanlder = dbService.getRepository<Task>(ds.UserInfo.get(userId as any)?.dbName, CollectionNames.TASKS);
1163
- // taskHanlder.updateOne({
1164
- // taskId: taskId
1165
- // },
1166
- // {
1167
- // "$set": {
1168
- // status: TaskStatus.Completed
1169
- // }
1170
- // });
1171
- const resultData = {
1172
- repoName,
1173
- headBranch,
1174
- targetBranch,
1175
- prNumber,
1176
- prUrl,
1177
- prTitle,
1178
- commitsAhead,
1179
- createdAt: new Date().toISOString()
1180
- };
1181
- // // Emit socket event to refresh PR status on client
1182
- // if (socket) {
1183
- // socket.emit('refresh_pr_status', { repoName, prNumber });
1184
- // }
1185
- // // Update database asynchronously (non-blocking)
1186
- // this.updatePRMetadata(userId, resultData);
1187
- return resultData;
1188
- }
1189
- catch (prError) {
1190
- // Check if PR already exists
1191
- if (prError.status === 422) {
1192
- // Fetch existing PR
1193
- try {
1194
- const existingPRs = await octokit.rest.pulls.list({
1195
- owner: gitOrgName,
1196
- repo: repoName,
1197
- head: `${gitOrgName}:${headBranch}`,
1198
- base: targetBranch,
1199
- state: 'open'
1200
- });
1201
- if (existingPRs.data.length > 0) {
1202
- const existingPR = existingPRs.data[0];
1203
- logger.info('Pull request already exists', {
1204
- repoName,
1205
- prNumber: existingPR.number,
1206
- 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'
1207
1216
  });
1208
- const resultData = {
1209
- repoName,
1210
- headBranch,
1211
- targetBranch,
1212
- prNumber: existingPR.number,
1213
- prUrl: existingPR.html_url,
1214
- prTitle: existingPR.title,
1215
- commitsAhead,
1216
- createdAt: existingPR.created_at,
1217
- alreadyExists: true
1218
- };
1219
- // Emit socket event to refresh PR status on client
1220
- if (socket) {
1221
- 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;
1222
1242
  }
1223
- // Update database asynchronously (non-blocking)
1224
- // this.updatePRMetadata(userId, resultData);
1225
- return resultData;
1243
+ }
1244
+ catch (fetchError) {
1245
+ logger.error('Failed to fetch existing PR', fetchError);
1226
1246
  }
1227
1247
  }
1228
- catch (fetchError) {
1229
- logger.error('Failed to fetch existing PR', fetchError);
1230
- }
1248
+ throw prError;
1231
1249
  }
1232
- throw prError;
1233
1250
  }
1234
- }
1235
- finally {
1236
- }
1251
+ finally {
1252
+ }
1253
+ }); // end withRepoLock
1237
1254
  }
1238
1255
  /**
1239
1256
  * Check if pull request exists
@@ -1242,35 +1259,37 @@ class GithubOperationsService {
1242
1259
  const repoPath = `${tool_server_1.folderPath}/${repoName}`;
1243
1260
  const installationToken = await fetchInstallationToken();
1244
1261
  const gitOrgName = await getGithubOrganizationName();
1245
- try {
1246
- const trimmedSourceBranch = (sourceBranch || '').trim();
1247
- const trimmedTargetBranch = (targetBranch || '').trim();
1248
- if (!trimmedSourceBranch || !trimmedTargetBranch) {
1249
- throw new Error('Both sourceBranch and targetBranch are required');
1250
- }
1251
- if (trimmedSourceBranch === trimmedTargetBranch) {
1252
- throw new Error('sourceBranch and targetBranch cannot be the same');
1253
- }
1254
- logger.info('Starting branch merge operation', {
1255
- repoName,
1256
- sourceBranch: trimmedSourceBranch,
1257
- targetBranch: trimmedTargetBranch,
1258
- pushToRemote
1259
- });
1260
- let result = await executeShell(`cd ${repoPath} && sudo git remote set-url origin https://x-access-token:${installationToken}@github.com/${gitOrgName}/${repoName}.git`);
1261
- if (!result.success) {
1262
- throw new Error(`Failed to set authenticated remote URL: ${result.error || result.output || 'Unknown error'}`);
1263
- }
1264
- const fetchResult = await executeShell(`cd ${repoPath} && sudo git fetch origin --prune`);
1265
- if (!fetchResult.success) {
1266
- throw new Error(`Failed to fetch remote branches: ${fetchResult.error || fetchResult.output || 'Unknown error'}`);
1267
- }
1268
- const resolvedSource = await this.resolveBranchReference(repoPath, repoName, trimmedSourceBranch);
1269
- const resolvedTarget = await this.resolveBranchReference(repoPath, repoName, trimmedTargetBranch);
1270
- const targetLocalBranch = resolvedTarget.displayBranch;
1271
- const safeTargetLocalBranch = this.escapeSingleQuotedShell(targetLocalBranch);
1272
- const safeSourceRef = this.escapeSingleQuotedShell(resolvedSource.resolvedRef);
1273
- 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 = `
1274
1293
  cd ${repoPath} &&
1275
1294
  TARGET_LOCAL='${safeTargetLocalBranch}' &&
1276
1295
  if sudo git show-ref --verify --quiet "refs/heads/$TARGET_LOCAL"; then
@@ -1285,174 +1304,178 @@ class GithubOperationsService {
1285
1304
  sudo git pull --ff-only origin "$TARGET_LOCAL" 2>/dev/null || true;
1286
1305
  fi
1287
1306
  `.replace(/\n\s+/g, ' ');
1288
- result = await executeShell(prepareBranchCommand);
1289
- if (!result.success) {
1290
- throw new Error(`Failed to prepare target branch for merge: ${result.error || result.output || 'Unknown error'}`);
1291
- }
1292
- const mergeCommand = `cd ${repoPath} && SOURCE_REF='${safeSourceRef}' && sudo git merge --no-ff --no-edit "$SOURCE_REF"`;
1293
- const mergeResult = await executeShell(mergeCommand);
1294
- if (!mergeResult.success) {
1295
- const conflictResult = await executeShell(`cd ${repoPath} && sudo git diff --name-only --diff-filter=U`);
1296
- const conflictFiles = (conflictResult.output || '')
1297
- .split('\n')
1298
- .map((line) => line.trim())
1299
- .filter((line) => line.length > 0);
1300
- if (conflictFiles.length > 0) {
1301
- logger.warn('Merge completed with conflicts', {
1302
- repoName,
1303
- sourceBranch: resolvedSource.displayBranch,
1304
- targetBranch: targetLocalBranch,
1305
- conflictFiles
1306
- });
1307
- return {
1308
- repoName,
1309
- sourceBranch: resolvedSource.displayBranch,
1310
- targetBranch: targetLocalBranch,
1311
- merged: false,
1312
- hasConflicts: true,
1313
- conflictFiles,
1314
- currentBranch: targetLocalBranch,
1315
- pushedToRemote: false,
1316
- mergedAt: new Date().toISOString(),
1317
- message: `Merge has conflicts. Resolve conflicts in target branch '${targetLocalBranch}'.`
1318
- };
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'}`);
1319
1310
  }
1320
- throw new Error(`Failed to merge branches: ${mergeResult.error || mergeResult.output || 'Unknown error'}`);
1321
- }
1322
- 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"`);
1323
- if (!summaryResult.success) {
1324
- throw new Error(`Merge succeeded but failed to fetch merge summary: ${summaryResult.error || summaryResult.output || 'Unknown error'}`);
1325
- }
1326
- const summaryOutput = summaryResult.output || '';
1327
- const commitHashMatch = summaryOutput.match(/COMMIT_HASH:([^\n]+)/);
1328
- const currentBranchMatch = summaryOutput.match(/CURRENT_BRANCH:([^\n]+)/);
1329
- const commitHash = commitHashMatch ? commitHashMatch[1].trim() : '';
1330
- const currentBranch = currentBranchMatch ? currentBranchMatch[1].trim() : targetLocalBranch;
1331
- let pushedToRemote = false;
1332
- if (pushToRemote) {
1333
- const pushResult = await executeShell(`cd ${repoPath} && sudo git push origin '${safeTargetLocalBranch}'`);
1334
- if (!pushResult.success) {
1335
- 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'}`);
1336
1340
  }
1337
- pushedToRemote = true;
1338
- }
1339
- logger.success('Branch merge completed successfully', {
1340
- repoName,
1341
- sourceBranch: resolvedSource.displayBranch,
1342
- targetBranch: targetLocalBranch,
1343
- commitHash,
1344
- pushedToRemote
1345
- });
1346
- if (socket) {
1347
- socket.emit('refresh_repo', { repoName });
1348
- }
1349
- return {
1350
- repoName,
1351
- sourceBranch: resolvedSource.displayBranch,
1352
- targetBranch: targetLocalBranch,
1353
- merged: true,
1354
- hasConflicts: false,
1355
- conflictFiles: [],
1356
- currentBranch,
1357
- commitHash,
1358
- pushedToRemote,
1359
- mergedAt: new Date().toISOString(),
1360
- message: pushedToRemote
1361
- ? `Merged '${resolvedSource.displayBranch}' into '${targetLocalBranch}' and pushed to remote.`
1362
- : `Merged '${resolvedSource.displayBranch}' into '${targetLocalBranch}' locally.`
1363
- };
1364
- }
1365
- finally {
1366
- try {
1367
- await executeShell(`cd ${repoPath} && sudo git remote set-url origin https://github.com/${gitOrgName}/${repoName}.git`);
1368
- }
1369
- catch (restoreError) {
1370
- 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', {
1371
1359
  repoName,
1372
- error: restoreError instanceof Error ? restoreError.message : String(restoreError)
1360
+ sourceBranch: resolvedSource.displayBranch,
1361
+ targetBranch: targetLocalBranch,
1362
+ commitHash,
1363
+ pushedToRemote
1373
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
+ };
1374
1383
  }
1375
- }
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
1376
1396
  }
1377
1397
  async checkPullRequestExists(repoName, targetBranch) {
1378
1398
  const repoPath = `${tool_server_1.folderPath}/${repoName}`;
1379
1399
  const installationToken = await fetchInstallationToken();
1380
1400
  const gitOrgName = await getGithubOrganizationName();
1381
- try {
1382
- logger.info('Checking if PR exists', { repoName, targetBranch });
1383
- // Get current branch and commits ahead
1384
- 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 = `
1385
1406
  cd ${repoPath} &&
1386
- 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 &&
1387
1409
  HEAD_BRANCH=$(sudo git rev-parse --abbrev-ref HEAD) &&
1388
1410
  sudo git fetch origin --prune 2>/dev/null || true &&
1389
1411
  sudo git fetch origin ${targetBranch} 2>/dev/null || true &&
1390
1412
  COMMITS_AHEAD=$(sudo git rev-list --count origin/${targetBranch}..$HEAD_BRANCH 2>/dev/null || echo 0) &&
1391
- sudo git remote set-url origin https://github.com/${gitOrgName}/${repoName}.git &&
1413
+ git remote set-url origin https://github.com/${gitOrgName}/${repoName}.git &&
1392
1414
  echo "HEAD_BRANCH:$HEAD_BRANCH" &&
1393
1415
  echo "COMMITS_AHEAD:$COMMITS_AHEAD"
1394
1416
  `.replace(/\n\s+/g, ' ');
1395
- const result = await executeShell(combinedCommand);
1396
- // Check for command execution failure
1397
- if (!result.success) {
1398
- logger.error('Failed to check PR status', {
1399
- repoName,
1400
- error: result.error,
1401
- code: result.code,
1402
- 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
1403
1438
  });
1404
- throw new Error(`Failed to check PR status: ${result.error}`);
1405
- }
1406
- const output = result.output || '';
1407
- const headBranchMatch = output.match(/HEAD_BRANCH:([^\n]+)/);
1408
- const commitsAheadMatch = output.match(/COMMITS_AHEAD:(\d+)/);
1409
- const headBranch = headBranchMatch ? headBranchMatch[1].trim() : 'unknown';
1410
- const commitsAhead = commitsAheadMatch ? parseInt(commitsAheadMatch[1]) : 0;
1411
- logger.info('Branch info retrieved', { headBranch, commitsAhead });
1412
- // Check for existing PR - using 'octokit' package (already installed)
1413
- const { Octokit } = require('octokit');
1414
- const octokit = new Octokit({
1415
- auth: installationToken
1416
- });
1417
- const existingPRs = await octokit.rest.pulls.list({
1418
- owner: gitOrgName,
1419
- repo: repoName,
1420
- head: `${gitOrgName}:${headBranch}`,
1421
- base: targetBranch,
1422
- state: 'open'
1423
- });
1424
- if (existingPRs.data.length > 0) {
1425
- const existingPR = existingPRs.data[0];
1426
- logger.info('Pull request exists', {
1427
- repoName,
1428
- prNumber: existingPR.number,
1429
- 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'
1430
1445
  });
1431
- return {
1432
- repoName,
1433
- headBranch,
1434
- targetBranch,
1435
- prExists: true,
1436
- prNumber: existingPR.number,
1437
- prUrl: existingPR.html_url,
1438
- prTitle: existingPR.title,
1439
- commitsAhead,
1440
- createdAt: existingPR.created_at
1441
- };
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
+ }
1442
1475
  }
1443
- else {
1444
- logger.info('No pull request found', { repoName, headBranch, targetBranch });
1445
- return {
1446
- repoName,
1447
- headBranch,
1448
- targetBranch,
1449
- prExists: false,
1450
- commitsAhead
1451
- };
1476
+ finally {
1452
1477
  }
1453
- }
1454
- finally {
1455
- }
1478
+ }); // end withRepoLock
1456
1479
  }
1457
1480
  /**
1458
1481
  * Get commit list with commit IDs for a branch