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