gitpadi 2.1.8 → 2.1.9

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/cli.js CHANGED
@@ -24,6 +24,7 @@ import * as applyForIssue from './commands/apply-for-issue.js';
24
24
  import { runBountyHunter } from './commands/bounty-hunter.js';
25
25
  import { dripsMenu } from './commands/drips.js';
26
26
  import { remindContributors } from './remind-contributors.js';
27
+ import { checkConflicts } from './conflict-checker.js';
27
28
  import * as gitlabIssues from './commands/gitlab-issues.js';
28
29
  import * as gitlabMRs from './commands/gitlab-mrs.js';
29
30
  import * as gitlabPipelines from './commands/gitlab-pipelines.js';
@@ -1077,6 +1078,7 @@ async function maintainerMenu() {
1077
1078
  { name: `${red('šŸš€')} ${bold('Releases')} ${dim('— create, list, tag releases')}`, value: 'releases' },
1078
1079
  { name: `${green('šŸŽÆ')} ${bold('Bounty Hunter')} ${dim('— auto-apply to Drips Wave & GrantFox')}`, value: 'hunt' },
1079
1080
  { name: `${cyan('šŸ””')} ${bold('Remind Contributors')} ${dim('— warn assignees with no PR (12h)')}`, value: 'remind' },
1081
+ { name: `${red('āš”ļø')} ${bold('Conflict Checker')} ${dim('— warn PRs with merge conflicts')}`, value: 'conflicts' },
1080
1082
  new inquirer.Separator(dim(' ─────────────────────────────────────────')),
1081
1083
  { name: `${dim('āš™ļø')} ${dim('Switch Repo')}`, value: 'switch' },
1082
1084
  { name: `${dim('⬅')} ${dim('Back to Mode Selector')}`, value: 'back' },
@@ -1104,6 +1106,8 @@ async function maintainerMenu() {
1104
1106
  await runBountyHunter({ platform: 'all', maxApplications: 3, dryRun: false });
1105
1107
  else if (category === 'remind')
1106
1108
  await safeMenu(remindContributors);
1109
+ else if (category === 'conflicts')
1110
+ await safeMenu(checkConflicts);
1107
1111
  else if (category === 'review-merge') {
1108
1112
  await ensureTargetRepo();
1109
1113
  const { prNum } = await ask([{ type: 'input', name: 'prNum', message: cyan('PR number to review:') }]);
@@ -0,0 +1,103 @@
1
+ import { initGitHub, getOctokit, getOwner, getRepo } from './core/github.js';
2
+ import chalk from 'chalk';
3
+ // Signature marker to prevent posting duplicate conflict warnings on the same PR
4
+ const SIG_CONFLICT = '<!-- gitpadi-conflict-warning -->';
5
+ export async function checkConflicts() {
6
+ console.log(chalk.bold('\nāš”ļø GitPadi Conflict Checker\n'));
7
+ try {
8
+ initGitHub();
9
+ const octokit = getOctokit();
10
+ const owner = getOwner();
11
+ const repo = getRepo();
12
+ if (!owner || !repo) {
13
+ console.error(chalk.red('āŒ Owner or Repo not found in environment/config.'));
14
+ process.exit(1);
15
+ }
16
+ console.log(chalk.dim(`šŸ”Ž Scanning open PRs in ${owner}/${repo} for merge conflicts...\n`));
17
+ // Fetch all open PRs
18
+ const { data: prs } = await octokit.pulls.list({
19
+ owner, repo, state: 'open', per_page: 100,
20
+ });
21
+ if (prs.length === 0) {
22
+ console.log(chalk.green('āœ… No open PRs found.'));
23
+ return;
24
+ }
25
+ let conflictCount = 0;
26
+ let skippedCount = 0;
27
+ let cleanCount = 0;
28
+ for (const pr of prs) {
29
+ const prNumber = pr.number;
30
+ const author = pr.user?.login ?? 'unknown';
31
+ process.stdout.write(chalk.cyan(` ā–ø PR #${prNumber}: "${pr.title.substring(0, 50)}" (@${author}) — `));
32
+ // Fetch the full PR to get the mergeable field
33
+ // GitHub computes mergeability lazily; we may need to retry once
34
+ let fullPr = (await octokit.pulls.get({ owner, repo, pull_number: prNumber })).data;
35
+ // If mergeability is still being computed (null), wait briefly and retry
36
+ if (fullPr.mergeable === null) {
37
+ await new Promise((r) => setTimeout(r, 3000));
38
+ fullPr = (await octokit.pulls.get({ owner, repo, pull_number: prNumber })).data;
39
+ }
40
+ const hasConflict = fullPr.mergeable === false && fullPr.mergeable_state === 'dirty';
41
+ if (!hasConflict) {
42
+ process.stdout.write(chalk.green('āœ… No conflict\n'));
43
+ cleanCount++;
44
+ continue;
45
+ }
46
+ // Check if we already posted a conflict warning on this PR
47
+ const { data: comments } = await octokit.issues.listComments({
48
+ owner, repo, issue_number: prNumber,
49
+ });
50
+ const alreadyWarned = comments.some((c) => c.body?.includes(SIG_CONFLICT));
51
+ if (alreadyWarned) {
52
+ process.stdout.write(chalk.dim('ā© Already warned\n'));
53
+ skippedCount++;
54
+ continue;
55
+ }
56
+ // Post the conflict warning comment
57
+ const conflictingFiles = fullPr.mergeable_state === 'dirty'
58
+ ? '\n\nTo see which files are conflicting, open the PR on GitHub and click **"Resolve conflicts"**.'
59
+ : '';
60
+ const message = [
61
+ `āš ļø **Merge Conflict Detected** — Hi @${author}!`,
62
+ ``,
63
+ `Your branch **\`${fullPr.head.ref}\`** has conflicts with the base branch **\`${fullPr.base.ref}\`** that must be resolved before this PR can be merged.`,
64
+ ``,
65
+ `**How to fix it:**`,
66
+ `\`\`\`bash`,
67
+ `# 1. Pull the latest base branch`,
68
+ `git checkout ${fullPr.base.ref}`,
69
+ `git pull origin ${fullPr.base.ref}`,
70
+ ``,
71
+ `# 2. Switch to your branch and merge/rebase`,
72
+ `git checkout ${fullPr.head.ref}`,
73
+ `git merge ${fullPr.base.ref}`,
74
+ ``,
75
+ `# 3. Resolve the conflicts, then commit and push`,
76
+ `git add .`,
77
+ `git commit -m "fix: resolve merge conflicts"`,
78
+ `git push origin ${fullPr.head.ref}`,
79
+ `\`\`\``,
80
+ conflictingFiles,
81
+ ``,
82
+ `If you need help resolving conflicts, check out the [GitHub guide on resolving conflicts](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/addressing-merge-conflicts/resolving-a-merge-conflict-using-the-command-line).`,
83
+ ``,
84
+ `${SIG_CONFLICT}`,
85
+ ].join('\n');
86
+ await octokit.issues.createComment({
87
+ owner, repo, issue_number: prNumber, body: message,
88
+ });
89
+ process.stdout.write(chalk.red('šŸ”“ Conflict — warning posted\n'));
90
+ conflictCount++;
91
+ }
92
+ console.log(chalk.bold(`\n✨ Conflict scan complete.`));
93
+ console.log(chalk.dim(` šŸ“Š Conflicts warned: ${conflictCount} | Already warned: ${skippedCount} | Clean: ${cleanCount}\n`));
94
+ }
95
+ catch (error) {
96
+ console.error(chalk.red(`\nāŒ Error during conflict scan: ${error.message}`));
97
+ process.exit(1);
98
+ }
99
+ }
100
+ // Standalone entry point (npx tsx src/conflict-checker.ts)
101
+ if (process.argv[1]?.endsWith('conflict-checker.ts') || process.argv[1]?.endsWith('conflict-checker.js')) {
102
+ checkConflicts();
103
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gitpadi",
3
- "version": "2.1.8",
3
+ "version": "2.1.9",
4
4
  "description": "GitPadi — AI-powered GitHub & GitLab management CLI. Fork repos, manage issues & PRs, score contributors, grade assignments, and automate everything. Powered by Anthropic Claude via GitLab Duo Agent Platform.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/cli.ts CHANGED
@@ -32,6 +32,7 @@ import * as applyForIssue from './commands/apply-for-issue.js';
32
32
  import { runBountyHunter } from './commands/bounty-hunter.js';
33
33
  import { dripsMenu } from './commands/drips.js';
34
34
  import { remindContributors } from './remind-contributors.js';
35
+ import { checkConflicts } from './conflict-checker.js';
35
36
  import * as gitlabIssues from './commands/gitlab-issues.js';
36
37
  import * as gitlabMRs from './commands/gitlab-mrs.js';
37
38
  import * as gitlabPipelines from './commands/gitlab-pipelines.js';
@@ -1122,6 +1123,7 @@ async function maintainerMenu() {
1122
1123
  { name: `${red('šŸš€')} ${bold('Releases')} ${dim('— create, list, tag releases')}`, value: 'releases' },
1123
1124
  { name: `${green('šŸŽÆ')} ${bold('Bounty Hunter')} ${dim('— auto-apply to Drips Wave & GrantFox')}`, value: 'hunt' },
1124
1125
  { name: `${cyan('šŸ””')} ${bold('Remind Contributors')} ${dim('— warn assignees with no PR (12h)')}`, value: 'remind' },
1126
+ { name: `${red('āš”ļø')} ${bold('Conflict Checker')} ${dim('— warn PRs with merge conflicts')}`, value: 'conflicts' },
1125
1127
  new inquirer.Separator(dim(' ─────────────────────────────────────────')),
1126
1128
  { name: `${dim('āš™ļø')} ${dim('Switch Repo')}`, value: 'switch' },
1127
1129
  { name: `${dim('⬅')} ${dim('Back to Mode Selector')}`, value: 'back' },
@@ -1144,6 +1146,7 @@ async function maintainerMenu() {
1144
1146
  else if (category === 'releases') await safeMenu(releaseMenu);
1145
1147
  else if (category === 'hunt') await runBountyHunter({ platform: 'all', maxApplications: 3, dryRun: false });
1146
1148
  else if (category === 'remind') await safeMenu(remindContributors);
1149
+ else if (category === 'conflicts') await safeMenu(checkConflicts);
1147
1150
  else if (category === 'review-merge') {
1148
1151
  await ensureTargetRepo();
1149
1152
  const { prNum } = await ask([{ type: 'input', name: 'prNum', message: cyan('PR number to review:') }]);
@@ -0,0 +1,126 @@
1
+ import { initGitHub, getOctokit, getOwner, getRepo } from './core/github.js';
2
+ import chalk from 'chalk';
3
+
4
+ // Signature marker to prevent posting duplicate conflict warnings on the same PR
5
+ const SIG_CONFLICT = '<!-- gitpadi-conflict-warning -->';
6
+
7
+ export async function checkConflicts() {
8
+ console.log(chalk.bold('\nāš”ļø GitPadi Conflict Checker\n'));
9
+
10
+ try {
11
+ initGitHub();
12
+ const octokit = getOctokit();
13
+ const owner = getOwner();
14
+ const repo = getRepo();
15
+
16
+ if (!owner || !repo) {
17
+ console.error(chalk.red('āŒ Owner or Repo not found in environment/config.'));
18
+ process.exit(1);
19
+ }
20
+
21
+ console.log(chalk.dim(`šŸ”Ž Scanning open PRs in ${owner}/${repo} for merge conflicts...\n`));
22
+
23
+ // Fetch all open PRs
24
+ const { data: prs } = await octokit.pulls.list({
25
+ owner, repo, state: 'open', per_page: 100,
26
+ });
27
+
28
+ if (prs.length === 0) {
29
+ console.log(chalk.green('āœ… No open PRs found.'));
30
+ return;
31
+ }
32
+
33
+ let conflictCount = 0;
34
+ let skippedCount = 0;
35
+ let cleanCount = 0;
36
+
37
+ for (const pr of prs) {
38
+ const prNumber = pr.number;
39
+ const author = pr.user?.login ?? 'unknown';
40
+
41
+ process.stdout.write(chalk.cyan(` ā–ø PR #${prNumber}: "${pr.title.substring(0, 50)}" (@${author}) — `));
42
+
43
+ // Fetch the full PR to get the mergeable field
44
+ // GitHub computes mergeability lazily; we may need to retry once
45
+ let fullPr = (await octokit.pulls.get({ owner, repo, pull_number: prNumber })).data;
46
+
47
+ // If mergeability is still being computed (null), wait briefly and retry
48
+ if (fullPr.mergeable === null) {
49
+ await new Promise((r) => setTimeout(r, 3000));
50
+ fullPr = (await octokit.pulls.get({ owner, repo, pull_number: prNumber })).data;
51
+ }
52
+
53
+ const hasConflict = fullPr.mergeable === false && fullPr.mergeable_state === 'dirty';
54
+
55
+ if (!hasConflict) {
56
+ process.stdout.write(chalk.green('āœ… No conflict\n'));
57
+ cleanCount++;
58
+ continue;
59
+ }
60
+
61
+ // Check if we already posted a conflict warning on this PR
62
+ const { data: comments } = await octokit.issues.listComments({
63
+ owner, repo, issue_number: prNumber,
64
+ });
65
+
66
+ const alreadyWarned = comments.some((c) => c.body?.includes(SIG_CONFLICT));
67
+
68
+ if (alreadyWarned) {
69
+ process.stdout.write(chalk.dim('ā© Already warned\n'));
70
+ skippedCount++;
71
+ continue;
72
+ }
73
+
74
+ // Post the conflict warning comment
75
+ const conflictingFiles = fullPr.mergeable_state === 'dirty'
76
+ ? '\n\nTo see which files are conflicting, open the PR on GitHub and click **"Resolve conflicts"**.'
77
+ : '';
78
+
79
+ const message = [
80
+ `āš ļø **Merge Conflict Detected** — Hi @${author}!`,
81
+ ``,
82
+ `Your branch **\`${fullPr.head.ref}\`** has conflicts with the base branch **\`${fullPr.base.ref}\`** that must be resolved before this PR can be merged.`,
83
+ ``,
84
+ `**How to fix it:**`,
85
+ `\`\`\`bash`,
86
+ `# 1. Pull the latest base branch`,
87
+ `git checkout ${fullPr.base.ref}`,
88
+ `git pull origin ${fullPr.base.ref}`,
89
+ ``,
90
+ `# 2. Switch to your branch and merge/rebase`,
91
+ `git checkout ${fullPr.head.ref}`,
92
+ `git merge ${fullPr.base.ref}`,
93
+ ``,
94
+ `# 3. Resolve the conflicts, then commit and push`,
95
+ `git add .`,
96
+ `git commit -m "fix: resolve merge conflicts"`,
97
+ `git push origin ${fullPr.head.ref}`,
98
+ `\`\`\``,
99
+ conflictingFiles,
100
+ ``,
101
+ `If you need help resolving conflicts, check out the [GitHub guide on resolving conflicts](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/addressing-merge-conflicts/resolving-a-merge-conflict-using-the-command-line).`,
102
+ ``,
103
+ `${SIG_CONFLICT}`,
104
+ ].join('\n');
105
+
106
+ await octokit.issues.createComment({
107
+ owner, repo, issue_number: prNumber, body: message,
108
+ });
109
+
110
+ process.stdout.write(chalk.red('šŸ”“ Conflict — warning posted\n'));
111
+ conflictCount++;
112
+ }
113
+
114
+ console.log(chalk.bold(`\n✨ Conflict scan complete.`));
115
+ console.log(chalk.dim(` šŸ“Š Conflicts warned: ${conflictCount} | Already warned: ${skippedCount} | Clean: ${cleanCount}\n`));
116
+
117
+ } catch (error: any) {
118
+ console.error(chalk.red(`\nāŒ Error during conflict scan: ${error.message}`));
119
+ process.exit(1);
120
+ }
121
+ }
122
+
123
+ // Standalone entry point (npx tsx src/conflict-checker.ts)
124
+ if (process.argv[1]?.endsWith('conflict-checker.ts') || process.argv[1]?.endsWith('conflict-checker.js')) {
125
+ checkConflicts();
126
+ }