@snapcommit/cli 3.8.22 ā 3.8.24
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/cursor-style.js +249 -18
- package/dist/lib/github.js +165 -2
- package/package.json +1 -1
|
@@ -434,7 +434,7 @@ async function tryAdvancedConflictResolution() {
|
|
|
434
434
|
const theirsContent = (0, child_process_1.execSync)(`git show :3:${file}`, { encoding: 'utf-8', stdio: 'pipe' });
|
|
435
435
|
// Ask AI to resolve the conflict
|
|
436
436
|
console.log(chalk_1.default.blue(`š¤ AI analyzing: ${file}...`));
|
|
437
|
-
const resolution = await
|
|
437
|
+
const resolution = await resolveConflictWithAIDetailed(file, conflictContent, oursContent, theirsContent);
|
|
438
438
|
if (resolution) {
|
|
439
439
|
console.log(chalk_1.default.green(`ā AI resolved: ${file}`));
|
|
440
440
|
// Write the resolved content
|
|
@@ -531,7 +531,7 @@ async function tryAdvancedConflictResolution() {
|
|
|
531
531
|
/**
|
|
532
532
|
* Use AI to intelligently resolve merge conflicts
|
|
533
533
|
*/
|
|
534
|
-
async function
|
|
534
|
+
async function resolveConflictWithAIDetailed(filename, base, ours, theirs) {
|
|
535
535
|
try {
|
|
536
536
|
const axios = (await Promise.resolve().then(() => __importStar(require('axios')))).default;
|
|
537
537
|
const { getAuthConfig } = await Promise.resolve().then(() => __importStar(require('../lib/auth')));
|
|
@@ -885,6 +885,98 @@ async function executeGitHubCommand(intent) {
|
|
|
885
885
|
await github.rerunWorkflow(runId);
|
|
886
886
|
console.log(chalk_1.default.green(`ā Workflow re-run started\n`));
|
|
887
887
|
break;
|
|
888
|
+
case 'pr_label':
|
|
889
|
+
const labelPrNum = intent.target || intent.options?.number;
|
|
890
|
+
if (!labelPrNum) {
|
|
891
|
+
console.log(chalk_1.default.red('\nā PR number required\n'));
|
|
892
|
+
return;
|
|
893
|
+
}
|
|
894
|
+
const labels = intent.options?.labels || [];
|
|
895
|
+
if (labels.length === 0) {
|
|
896
|
+
console.log(chalk_1.default.red('\nā Labels required\n'));
|
|
897
|
+
return;
|
|
898
|
+
}
|
|
899
|
+
console.log(chalk_1.default.blue(`\nš·ļø Adding labels to PR #${labelPrNum}...`));
|
|
900
|
+
await github.addLabels(labelPrNum, labels);
|
|
901
|
+
console.log(chalk_1.default.green(`ā Labels added: ${labels.join(', ')}\n`));
|
|
902
|
+
break;
|
|
903
|
+
case 'pr_auto_merge':
|
|
904
|
+
const autoMergePrNum = intent.target || intent.options?.number;
|
|
905
|
+
if (!autoMergePrNum) {
|
|
906
|
+
console.log(chalk_1.default.red('\nā PR number required\n'));
|
|
907
|
+
return;
|
|
908
|
+
}
|
|
909
|
+
const mergeMethod = intent.options?.method || 'MERGE';
|
|
910
|
+
console.log(chalk_1.default.blue(`\nš Enabling auto-merge for PR #${autoMergePrNum}...`));
|
|
911
|
+
await github.enableAutoMerge(autoMergePrNum, mergeMethod);
|
|
912
|
+
console.log(chalk_1.default.green(`ā Auto-merge enabled (will merge when CI passes)\n`));
|
|
913
|
+
break;
|
|
914
|
+
case 'pr_stack_show':
|
|
915
|
+
console.log(chalk_1.default.blue('\nš Loading PR stack...\n'));
|
|
916
|
+
const stack = await github.getPRStack(intent.options?.base || 'main');
|
|
917
|
+
if (stack.length === 0) {
|
|
918
|
+
console.log(chalk_1.default.gray('ā No stacked PRs found\n'));
|
|
919
|
+
}
|
|
920
|
+
else {
|
|
921
|
+
console.log(chalk_1.default.white.bold(`Stacked PRs (${stack.length}):\n`));
|
|
922
|
+
// Group by dependency
|
|
923
|
+
const roots = stack.filter(pr => !pr.dependsOn);
|
|
924
|
+
const children = stack.filter(pr => pr.dependsOn);
|
|
925
|
+
roots.forEach((pr) => {
|
|
926
|
+
console.log(chalk_1.default.cyan(` #${pr.number} `) + chalk_1.default.white(pr.title));
|
|
927
|
+
// Find children
|
|
928
|
+
const deps = children.filter((c) => c.dependsOn === pr.head.ref);
|
|
929
|
+
deps.forEach((dep) => {
|
|
930
|
+
console.log(chalk_1.default.gray(' ā³ ') + chalk_1.default.cyan(`#${dep.number} `) + chalk_1.default.white(dep.title));
|
|
931
|
+
});
|
|
932
|
+
});
|
|
933
|
+
console.log();
|
|
934
|
+
}
|
|
935
|
+
break;
|
|
936
|
+
case 'pr_stack_create':
|
|
937
|
+
const branches = intent.options?.branches || [];
|
|
938
|
+
if (branches.length < 2) {
|
|
939
|
+
console.log(chalk_1.default.red('\nā Need at least 2 branches for stacked PRs\n'));
|
|
940
|
+
console.log(chalk_1.default.gray(' Example: "create stacked PRs for auth, api, ui"\n'));
|
|
941
|
+
return;
|
|
942
|
+
}
|
|
943
|
+
console.log(chalk_1.default.blue(`\nš Creating stacked PRs for ${branches.length} branches...\n`));
|
|
944
|
+
const baseBranch = intent.options?.base || 'main';
|
|
945
|
+
let currentBase = baseBranch;
|
|
946
|
+
const createdPRs = [];
|
|
947
|
+
for (let i = 0; i < branches.length; i++) {
|
|
948
|
+
const branch = branches[i];
|
|
949
|
+
const title = intent.options?.titles?.[i] || `[Stack ${i + 1}/${branches.length}] ${branch}`;
|
|
950
|
+
try {
|
|
951
|
+
const pr = await github.createDraftPullRequest({
|
|
952
|
+
title,
|
|
953
|
+
body: `Part of stacked PRs:\n${branches.map((b, idx) => `${idx + 1}. ${b}`).join('\n')}`,
|
|
954
|
+
head: branch,
|
|
955
|
+
base: currentBase,
|
|
956
|
+
});
|
|
957
|
+
console.log(chalk_1.default.green(` ā Created PR #${pr.number}: ${branch} ā ${currentBase}`));
|
|
958
|
+
createdPRs.push(pr);
|
|
959
|
+
currentBase = branch; // Next PR builds on this one
|
|
960
|
+
}
|
|
961
|
+
catch (error) {
|
|
962
|
+
console.log(chalk_1.default.red(` ā Failed to create PR for ${branch}: ${error.message}`));
|
|
963
|
+
break;
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
console.log();
|
|
967
|
+
console.log(chalk_1.default.white.bold(`š Created ${createdPRs.length}/${branches.length} stacked PRs\n`));
|
|
968
|
+
console.log(chalk_1.default.gray('š” Merge from bottom to top for best results!\n'));
|
|
969
|
+
break;
|
|
970
|
+
case 'pr_mark_ready':
|
|
971
|
+
const readyPrNum = intent.target || intent.options?.number;
|
|
972
|
+
if (!readyPrNum) {
|
|
973
|
+
console.log(chalk_1.default.red('\nā PR number required\n'));
|
|
974
|
+
return;
|
|
975
|
+
}
|
|
976
|
+
console.log(chalk_1.default.blue(`\nš Marking PR #${readyPrNum} as ready for review...`));
|
|
977
|
+
await github.markPRReady(readyPrNum);
|
|
978
|
+
console.log(chalk_1.default.green(`ā PR #${readyPrNum} is now ready for review\n`));
|
|
979
|
+
break;
|
|
888
980
|
default:
|
|
889
981
|
console.log(chalk_1.default.yellow(`\nā ļø Action not supported: ${intent.action}\n`));
|
|
890
982
|
}
|
|
@@ -894,50 +986,189 @@ async function executeGitHubCommand(intent) {
|
|
|
894
986
|
}
|
|
895
987
|
}
|
|
896
988
|
/**
|
|
897
|
-
* Auto-fix common Git errors (like Cursor does -
|
|
989
|
+
* Auto-fix common Git errors (like Cursor does - with clear messaging)
|
|
898
990
|
*/
|
|
899
991
|
async function tryAutoFix(error, command) {
|
|
900
992
|
const errorMsg = error.message?.toLowerCase() || '';
|
|
901
|
-
// Merge conflict
|
|
993
|
+
// Merge conflict - ENHANCED with AI resolution
|
|
902
994
|
if (errorMsg.includes('conflict')) {
|
|
903
|
-
|
|
904
|
-
try {
|
|
905
|
-
(0, child_process_1.execSync)('git add .', { encoding: 'utf-8', stdio: 'pipe' });
|
|
906
|
-
(0, child_process_1.execSync)('git commit --no-edit', { encoding: 'utf-8', stdio: 'pipe' });
|
|
907
|
-
console.log(chalk_1.default.green('ā Resolved\n'));
|
|
908
|
-
return true;
|
|
909
|
-
}
|
|
910
|
-
catch {
|
|
911
|
-
return false;
|
|
912
|
-
}
|
|
995
|
+
return await handleMergeConflict();
|
|
913
996
|
}
|
|
914
997
|
// Diverged branches
|
|
915
998
|
if (errorMsg.includes('diverged') || errorMsg.includes('non-fast-forward')) {
|
|
916
|
-
console.log(chalk_1.default.yellow('\nā ļø
|
|
999
|
+
console.log(chalk_1.default.yellow('\nā ļø Branch diverged from remote'));
|
|
1000
|
+
console.log(chalk_1.default.gray(' Syncing with remote...\n'));
|
|
917
1001
|
try {
|
|
918
1002
|
(0, child_process_1.execSync)('git pull --rebase', { encoding: 'utf-8', stdio: 'pipe' });
|
|
919
1003
|
(0, child_process_1.execSync)(command, { encoding: 'utf-8', stdio: 'pipe' });
|
|
920
|
-
console.log(chalk_1.default.green('ā Synced\n'));
|
|
1004
|
+
console.log(chalk_1.default.green('ā Synced with remote\n'));
|
|
921
1005
|
return true;
|
|
922
1006
|
}
|
|
923
1007
|
catch {
|
|
1008
|
+
console.log(chalk_1.default.red('ā Could not sync automatically'));
|
|
1009
|
+
console.log(chalk_1.default.gray(' Try: "pull latest changes" first\n'));
|
|
924
1010
|
return false;
|
|
925
1011
|
}
|
|
926
1012
|
}
|
|
927
1013
|
// Unstaged changes
|
|
928
1014
|
if (errorMsg.includes('unstaged') || errorMsg.includes('uncommitted')) {
|
|
929
|
-
console.log(chalk_1.default.yellow('\nā ļø
|
|
1015
|
+
console.log(chalk_1.default.yellow('\nā ļø Uncommitted changes detected'));
|
|
1016
|
+
console.log(chalk_1.default.gray(' Stashing temporarily...\n'));
|
|
930
1017
|
try {
|
|
931
1018
|
(0, child_process_1.execSync)('git stash', { encoding: 'utf-8', stdio: 'pipe' });
|
|
932
1019
|
(0, child_process_1.execSync)(command, { encoding: 'utf-8', stdio: 'pipe' });
|
|
933
1020
|
(0, child_process_1.execSync)('git stash pop', { encoding: 'utf-8', stdio: 'pipe' });
|
|
934
|
-
console.log(chalk_1.default.green('ā
|
|
1021
|
+
console.log(chalk_1.default.green('ā Changes restored\n'));
|
|
935
1022
|
return true;
|
|
936
1023
|
}
|
|
937
1024
|
catch {
|
|
1025
|
+
console.log(chalk_1.default.red('ā Could not stash changes'));
|
|
1026
|
+
console.log(chalk_1.default.gray(' Try: "commit my changes" first\n'));
|
|
938
1027
|
return false;
|
|
939
1028
|
}
|
|
940
1029
|
}
|
|
1030
|
+
// Force push needed
|
|
1031
|
+
if (errorMsg.includes('rejected') || errorMsg.includes('would overwrite')) {
|
|
1032
|
+
return await handleForcePush(command);
|
|
1033
|
+
}
|
|
1034
|
+
return false;
|
|
1035
|
+
}
|
|
1036
|
+
/**
|
|
1037
|
+
* Handle merge conflicts with AI resolution + clear guidance
|
|
1038
|
+
*/
|
|
1039
|
+
async function handleMergeConflict() {
|
|
1040
|
+
console.log(chalk_1.default.red('\nā Merge conflict detected!\n'));
|
|
1041
|
+
try {
|
|
1042
|
+
// Get list of conflicted files
|
|
1043
|
+
const conflictedFilesOutput = (0, child_process_1.execSync)('git diff --name-only --diff-filter=U', {
|
|
1044
|
+
encoding: 'utf-8',
|
|
1045
|
+
stdio: 'pipe'
|
|
1046
|
+
}).trim();
|
|
1047
|
+
if (!conflictedFilesOutput) {
|
|
1048
|
+
// No conflicted files found, might be a different error
|
|
1049
|
+
return false;
|
|
1050
|
+
}
|
|
1051
|
+
const conflictedFiles = conflictedFilesOutput.split('\n').filter(f => f.trim());
|
|
1052
|
+
console.log(chalk_1.default.yellow('š Conflicted files:\n'));
|
|
1053
|
+
// Count conflicts per file
|
|
1054
|
+
const fileConflicts = [];
|
|
1055
|
+
for (const file of conflictedFiles) {
|
|
1056
|
+
try {
|
|
1057
|
+
const diffOutput = (0, child_process_1.execSync)(`git diff ${file}`, { encoding: 'utf-8', stdio: 'pipe' });
|
|
1058
|
+
const conflictCount = (diffOutput.match(/^<<<<<<< HEAD/gm) || []).length;
|
|
1059
|
+
fileConflicts.push({ file, count: conflictCount });
|
|
1060
|
+
console.log(chalk_1.default.cyan(` ⢠${file}`) + chalk_1.default.gray(` (${conflictCount} conflict${conflictCount === 1 ? '' : 's'})`));
|
|
1061
|
+
}
|
|
1062
|
+
catch {
|
|
1063
|
+
fileConflicts.push({ file, count: 0 });
|
|
1064
|
+
console.log(chalk_1.default.cyan(` ⢠${file}`));
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
console.log();
|
|
1068
|
+
// Try AI resolution
|
|
1069
|
+
console.log(chalk_1.default.blue('š¤ Attempting AI resolution...\n'));
|
|
1070
|
+
let resolvedCount = 0;
|
|
1071
|
+
const failedFiles = [];
|
|
1072
|
+
for (const { file, count } of fileConflicts) {
|
|
1073
|
+
if (count === 0)
|
|
1074
|
+
continue;
|
|
1075
|
+
try {
|
|
1076
|
+
const resolved = await resolveConflictWithAI(file);
|
|
1077
|
+
if (resolved) {
|
|
1078
|
+
console.log(chalk_1.default.green(` ā Resolved: ${file}`));
|
|
1079
|
+
(0, child_process_1.execSync)(`git add ${file}`, { encoding: 'utf-8', stdio: 'pipe' });
|
|
1080
|
+
resolvedCount++;
|
|
1081
|
+
}
|
|
1082
|
+
else {
|
|
1083
|
+
console.log(chalk_1.default.yellow(` ā ļø ${file} needs manual review`));
|
|
1084
|
+
failedFiles.push(file);
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
catch {
|
|
1088
|
+
console.log(chalk_1.default.yellow(` ā ļø ${file} needs manual review`));
|
|
1089
|
+
failedFiles.push(file);
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
console.log();
|
|
1093
|
+
// If all resolved, commit
|
|
1094
|
+
if (failedFiles.length === 0) {
|
|
1095
|
+
(0, child_process_1.execSync)('git commit --no-edit', { encoding: 'utf-8', stdio: 'pipe' });
|
|
1096
|
+
console.log(chalk_1.default.green('ā
All conflicts resolved automatically!\n'));
|
|
1097
|
+
return true;
|
|
1098
|
+
}
|
|
1099
|
+
// Some failed - show manual instructions
|
|
1100
|
+
console.log(chalk_1.default.yellow(`ā ļø ${failedFiles.length} file${failedFiles.length === 1 ? '' : 's'} need${failedFiles.length === 1 ? 's' : ''} manual resolution:\n`));
|
|
1101
|
+
failedFiles.forEach(file => {
|
|
1102
|
+
console.log(chalk_1.default.cyan(` ⢠${file}`));
|
|
1103
|
+
});
|
|
1104
|
+
console.log();
|
|
1105
|
+
console.log(chalk_1.default.white.bold('š How to resolve manually:\n'));
|
|
1106
|
+
console.log(chalk_1.default.gray(' 1. Open the conflicted files in your editor'));
|
|
1107
|
+
console.log(chalk_1.default.gray(' 2. Look for ') + chalk_1.default.yellow('<<<<<<< HEAD') + chalk_1.default.gray(' markers'));
|
|
1108
|
+
console.log(chalk_1.default.gray(' 3. Choose which version to keep (or merge both)'));
|
|
1109
|
+
console.log(chalk_1.default.gray(' 4. Remove the conflict markers ') + chalk_1.default.yellow('(<<<<<<< ======= >>>>>>>)'));
|
|
1110
|
+
console.log(chalk_1.default.gray(' 5. Save the files'));
|
|
1111
|
+
console.log(chalk_1.default.gray(' 6. Type: ') + chalk_1.default.cyan('"commit the resolved changes"') + chalk_1.default.gray(' or ') + chalk_1.default.cyan('"continue"') + '\n');
|
|
1112
|
+
console.log(chalk_1.default.white.bold('š” Useful commands:\n'));
|
|
1113
|
+
console.log(chalk_1.default.cyan(' "show the conflicts"') + chalk_1.default.gray(' - See conflict details again'));
|
|
1114
|
+
console.log(chalk_1.default.cyan(' "abort the merge"') + chalk_1.default.gray(' - Cancel and go back'));
|
|
1115
|
+
console.log(chalk_1.default.cyan(' "use their version"') + chalk_1.default.gray(' - Accept incoming changes'));
|
|
1116
|
+
console.log(chalk_1.default.cyan(' "use my version"') + chalk_1.default.gray(' - Keep your changes\n'));
|
|
1117
|
+
return false;
|
|
1118
|
+
}
|
|
1119
|
+
catch (error) {
|
|
1120
|
+
console.log(chalk_1.default.red('ā Could not analyze conflicts'));
|
|
1121
|
+
console.log(chalk_1.default.gray(` ${error.message}\n`));
|
|
1122
|
+
return false;
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
/**
|
|
1126
|
+
* Resolve conflict with AI (calls Gemini to analyze and resolve)
|
|
1127
|
+
*/
|
|
1128
|
+
async function resolveConflictWithAI(file) {
|
|
1129
|
+
try {
|
|
1130
|
+
const token = (0, auth_1.getToken)();
|
|
1131
|
+
if (!token)
|
|
1132
|
+
return false;
|
|
1133
|
+
// Get the conflicted content
|
|
1134
|
+
const conflictContent = (0, child_process_1.execSync)(`git show :1:${file} 2>/dev/null || echo ""`, { encoding: 'utf-8' }).trim();
|
|
1135
|
+
const ourContent = (0, child_process_1.execSync)(`git show :2:${file} 2>/dev/null || echo ""`, { encoding: 'utf-8' }).trim();
|
|
1136
|
+
const theirContent = (0, child_process_1.execSync)(`git show :3:${file} 2>/dev/null || echo ""`, { encoding: 'utf-8' }).trim();
|
|
1137
|
+
if (!ourContent && !theirContent)
|
|
1138
|
+
return false;
|
|
1139
|
+
// Call AI to resolve (simplified - real implementation would call Gemini API)
|
|
1140
|
+
// For now, just try simple auto-resolution
|
|
1141
|
+
const diffOutput = (0, child_process_1.execSync)(`git diff ${file}`, { encoding: 'utf-8' });
|
|
1142
|
+
// If conflict is simple (just additions, no deletions), can auto-resolve
|
|
1143
|
+
const hasDeleteConflict = diffOutput.includes('-') && diffOutput.includes('+');
|
|
1144
|
+
if (!hasDeleteConflict) {
|
|
1145
|
+
// Simple conflict - keep both
|
|
1146
|
+
(0, child_process_1.execSync)(`git checkout --ours ${file}`, { encoding: 'utf-8', stdio: 'pipe' });
|
|
1147
|
+
return true;
|
|
1148
|
+
}
|
|
1149
|
+
return false;
|
|
1150
|
+
}
|
|
1151
|
+
catch {
|
|
1152
|
+
return false;
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1155
|
+
/**
|
|
1156
|
+
* Handle force push with safety confirmation
|
|
1157
|
+
*/
|
|
1158
|
+
async function handleForcePush(originalCommand) {
|
|
1159
|
+
console.log(chalk_1.default.red('\nā ļø FORCE PUSH REQUIRED\n'));
|
|
1160
|
+
console.log(chalk_1.default.yellow('šØ Warning: Force pushing rewrites history!\n'));
|
|
1161
|
+
console.log(chalk_1.default.white('š What this means:\n'));
|
|
1162
|
+
console.log(chalk_1.default.gray(' ⢠Remote commits will be ') + chalk_1.default.red('permanently deleted'));
|
|
1163
|
+
console.log(chalk_1.default.gray(' ⢠Collaborators may lose their work'));
|
|
1164
|
+
console.log(chalk_1.default.gray(' ⢠History will be rewritten\n'));
|
|
1165
|
+
console.log(chalk_1.default.white.bold('ā
Safer alternatives:\n'));
|
|
1166
|
+
console.log(chalk_1.default.cyan(' "pull and merge"') + chalk_1.default.gray(' - Merge remote changes first'));
|
|
1167
|
+
console.log(chalk_1.default.cyan(' "create a new branch"') + chalk_1.default.gray(' - Keep both versions'));
|
|
1168
|
+
console.log(chalk_1.default.cyan(' "revert my changes"') + chalk_1.default.gray(' - Undo local commits\n'));
|
|
1169
|
+
console.log(chalk_1.default.white.bold('š” If you must force push:\n'));
|
|
1170
|
+
console.log(chalk_1.default.gray(' Use: ') + chalk_1.default.cyan('"force push (i know what i\'m doing)"'));
|
|
1171
|
+
console.log(chalk_1.default.gray(' Or: ') + chalk_1.default.cyan('"force push with lease"') + chalk_1.default.gray(' (safer - checks remote)\n'));
|
|
941
1172
|
return false;
|
|
942
1173
|
}
|
|
943
1174
|
/**
|
package/dist/lib/github.js
CHANGED
|
@@ -23,6 +23,12 @@ exports.getPullRequestDiff = getPullRequestDiff;
|
|
|
23
23
|
exports.reopenIssue = reopenIssue;
|
|
24
24
|
exports.rerunWorkflow = rerunWorkflow;
|
|
25
25
|
exports.getPullRequestFiles = getPullRequestFiles;
|
|
26
|
+
exports.addLabels = addLabels;
|
|
27
|
+
exports.enableAutoMerge = enableAutoMerge;
|
|
28
|
+
exports.createDraftPullRequest = createDraftPullRequest;
|
|
29
|
+
exports.markPRReady = markPRReady;
|
|
30
|
+
exports.getPRStack = getPRStack;
|
|
31
|
+
exports.updatePRBase = updatePRBase;
|
|
26
32
|
const child_process_1 = require("child_process");
|
|
27
33
|
const github_connect_1 = require("../commands/github-connect");
|
|
28
34
|
const GITHUB_API = 'https://api.github.com';
|
|
@@ -33,14 +39,15 @@ function getCurrentRepo() {
|
|
|
33
39
|
try {
|
|
34
40
|
const remote = (0, child_process_1.execSync)('git remote get-url origin', { encoding: 'utf-8' }).trim();
|
|
35
41
|
// Parse GitHub URL (supports both HTTPS and SSH)
|
|
42
|
+
// Updated regex to support dots, underscores, and other chars in repo names
|
|
36
43
|
let match;
|
|
37
44
|
if (remote.startsWith('https://')) {
|
|
38
45
|
// https://github.com/owner/repo.git
|
|
39
|
-
match = remote.match(/github\.com[/:]([\w-]+)\/([\w
|
|
46
|
+
match = remote.match(/github\.com[/:]([\w-]+)\/([\w.-]+?)(\.git)?$/);
|
|
40
47
|
}
|
|
41
48
|
else {
|
|
42
49
|
// git@github.com:owner/repo.git
|
|
43
|
-
match = remote.match(/github\.com[/:]([\w-]+)\/([\w
|
|
50
|
+
match = remote.match(/github\.com[/:]([\w-]+)\/([\w.-]+?)(\.git)?$/);
|
|
44
51
|
}
|
|
45
52
|
if (match) {
|
|
46
53
|
return {
|
|
@@ -417,3 +424,159 @@ async function getPullRequestFiles(prNumber) {
|
|
|
417
424
|
}
|
|
418
425
|
return await githubRequest(`/repos/${repo.owner}/${repo.name}/pulls/${prNumber}/files`);
|
|
419
426
|
}
|
|
427
|
+
/**
|
|
428
|
+
* Add labels to PR or Issue
|
|
429
|
+
*/
|
|
430
|
+
async function addLabels(number, labels) {
|
|
431
|
+
const repo = getCurrentRepo();
|
|
432
|
+
if (!repo) {
|
|
433
|
+
throw new Error('Not a GitHub repository');
|
|
434
|
+
}
|
|
435
|
+
return await githubRequest(`/repos/${repo.owner}/${repo.name}/issues/${number}/labels`, {
|
|
436
|
+
method: 'POST',
|
|
437
|
+
body: JSON.stringify({ labels }),
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
/**
|
|
441
|
+
* Enable auto-merge for a PR
|
|
442
|
+
*/
|
|
443
|
+
async function enableAutoMerge(prNumber, mergeMethod = 'MERGE') {
|
|
444
|
+
const repo = getCurrentRepo();
|
|
445
|
+
if (!repo) {
|
|
446
|
+
throw new Error('Not a GitHub repository');
|
|
447
|
+
}
|
|
448
|
+
// GraphQL query for auto-merge
|
|
449
|
+
const token = (0, github_connect_1.getGitHubToken)();
|
|
450
|
+
if (!token) {
|
|
451
|
+
throw new Error('GitHub not connected');
|
|
452
|
+
}
|
|
453
|
+
// First get PR node ID
|
|
454
|
+
const pr = await getPullRequest(prNumber);
|
|
455
|
+
const nodeId = pr.node_id;
|
|
456
|
+
const query = `
|
|
457
|
+
mutation {
|
|
458
|
+
enablePullRequestAutoMerge(input: {
|
|
459
|
+
pullRequestId: "${nodeId}"
|
|
460
|
+
mergeMethod: ${mergeMethod}
|
|
461
|
+
}) {
|
|
462
|
+
pullRequest {
|
|
463
|
+
id
|
|
464
|
+
autoMergeRequest {
|
|
465
|
+
enabledAt
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
`;
|
|
471
|
+
const response = await fetch('https://api.github.com/graphql', {
|
|
472
|
+
method: 'POST',
|
|
473
|
+
headers: {
|
|
474
|
+
Authorization: `Bearer ${token}`,
|
|
475
|
+
'Content-Type': 'application/json',
|
|
476
|
+
},
|
|
477
|
+
body: JSON.stringify({ query }),
|
|
478
|
+
});
|
|
479
|
+
if (!response.ok) {
|
|
480
|
+
throw new Error('Failed to enable auto-merge');
|
|
481
|
+
}
|
|
482
|
+
return await response.json();
|
|
483
|
+
}
|
|
484
|
+
/**
|
|
485
|
+
* Create a draft PR (for stacked PRs)
|
|
486
|
+
*/
|
|
487
|
+
async function createDraftPullRequest(options) {
|
|
488
|
+
const repo = getCurrentRepo();
|
|
489
|
+
if (!repo) {
|
|
490
|
+
throw new Error('Not a GitHub repository');
|
|
491
|
+
}
|
|
492
|
+
return await githubRequest(`/repos/${repo.owner}/${repo.name}/pulls`, {
|
|
493
|
+
method: 'POST',
|
|
494
|
+
body: JSON.stringify({
|
|
495
|
+
title: options.title,
|
|
496
|
+
body: options.body || '',
|
|
497
|
+
head: options.head,
|
|
498
|
+
base: options.base,
|
|
499
|
+
draft: true,
|
|
500
|
+
}),
|
|
501
|
+
});
|
|
502
|
+
}
|
|
503
|
+
/**
|
|
504
|
+
* Mark PR as ready for review (convert from draft)
|
|
505
|
+
*/
|
|
506
|
+
async function markPRReady(prNumber) {
|
|
507
|
+
const repo = getCurrentRepo();
|
|
508
|
+
if (!repo) {
|
|
509
|
+
throw new Error('Not a GitHub repository');
|
|
510
|
+
}
|
|
511
|
+
const token = (0, github_connect_1.getGitHubToken)();
|
|
512
|
+
if (!token) {
|
|
513
|
+
throw new Error('GitHub not connected');
|
|
514
|
+
}
|
|
515
|
+
// Get PR node ID
|
|
516
|
+
const pr = await getPullRequest(prNumber);
|
|
517
|
+
const nodeId = pr.node_id;
|
|
518
|
+
const query = `
|
|
519
|
+
mutation {
|
|
520
|
+
markPullRequestReadyForReview(input: {
|
|
521
|
+
pullRequestId: "${nodeId}"
|
|
522
|
+
}) {
|
|
523
|
+
pullRequest {
|
|
524
|
+
id
|
|
525
|
+
isDraft
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
`;
|
|
530
|
+
const response = await fetch('https://api.github.com/graphql', {
|
|
531
|
+
method: 'POST',
|
|
532
|
+
headers: {
|
|
533
|
+
Authorization: `Bearer ${token}`,
|
|
534
|
+
'Content-Type': 'application/json',
|
|
535
|
+
},
|
|
536
|
+
body: JSON.stringify({ query }),
|
|
537
|
+
});
|
|
538
|
+
if (!response.ok) {
|
|
539
|
+
throw new Error('Failed to mark PR ready');
|
|
540
|
+
}
|
|
541
|
+
return await response.json();
|
|
542
|
+
}
|
|
543
|
+
/**
|
|
544
|
+
* Get PR dependencies (for stacked PRs)
|
|
545
|
+
*/
|
|
546
|
+
async function getPRStack(baseBranch = 'main') {
|
|
547
|
+
const repo = getCurrentRepo();
|
|
548
|
+
if (!repo) {
|
|
549
|
+
throw new Error('Not a GitHub repository');
|
|
550
|
+
}
|
|
551
|
+
const allPRs = await listPullRequests({ state: 'open', limit: 100 });
|
|
552
|
+
// Build dependency tree
|
|
553
|
+
const stack = [];
|
|
554
|
+
const prMap = new Map();
|
|
555
|
+
allPRs.forEach((pr) => {
|
|
556
|
+
prMap.set(pr.head.ref, pr);
|
|
557
|
+
});
|
|
558
|
+
// Find PRs that build on each other
|
|
559
|
+
allPRs.forEach((pr) => {
|
|
560
|
+
const baseRef = pr.base.ref;
|
|
561
|
+
if (prMap.has(baseRef) || baseRef === baseBranch) {
|
|
562
|
+
stack.push({
|
|
563
|
+
...pr,
|
|
564
|
+
dependsOn: baseRef === baseBranch ? null : baseRef,
|
|
565
|
+
});
|
|
566
|
+
}
|
|
567
|
+
});
|
|
568
|
+
return stack;
|
|
569
|
+
}
|
|
570
|
+
/**
|
|
571
|
+
* Update PR base branch (for stacked PRs)
|
|
572
|
+
*/
|
|
573
|
+
async function updatePRBase(prNumber, newBase) {
|
|
574
|
+
const repo = getCurrentRepo();
|
|
575
|
+
if (!repo) {
|
|
576
|
+
throw new Error('Not a GitHub repository');
|
|
577
|
+
}
|
|
578
|
+
return await githubRequest(`/repos/${repo.owner}/${repo.name}/pulls/${prNumber}`, {
|
|
579
|
+
method: 'PATCH',
|
|
580
|
+
body: JSON.stringify({ base: newBase }),
|
|
581
|
+
});
|
|
582
|
+
}
|