|
1 | 1 | #!/usr/bin/env node |
2 | 2 |
|
3 | 3 | import { execSync } from 'child_process'; |
4 | | -import { input, confirm } from '@inquirer/prompts'; |
| 4 | +import { input, confirm, search } from '@inquirer/prompts'; |
5 | 5 | import twig from 'twig'; |
6 | 6 | import { promisify } from 'util'; |
7 | 7 | import { existsSync } from 'fs'; |
@@ -54,6 +54,44 @@ export function getCurrentBranch() { |
54 | 54 | } |
55 | 55 | } |
56 | 56 |
|
| 57 | +// Get default branch name |
| 58 | +export function getDefaultBranch() { |
| 59 | + try { |
| 60 | + // Try to get the remote's default branch |
| 61 | + // First get the default remote (usually origin) |
| 62 | + const remote = execSync('git remote').toString().trim().split('\n')[0]; |
| 63 | + |
| 64 | + // Then get the default branch (what HEAD points to) |
| 65 | + const output = execSync(`git remote show ${remote} | grep "HEAD branch"`).toString().trim(); |
| 66 | + const match = output.match(/HEAD branch:\s*(.+)$/); |
| 67 | + |
| 68 | + if (match && match[1]) { |
| 69 | + return match[1]; |
| 70 | + } |
| 71 | + |
| 72 | + // Fallback to 'main' or 'master' if we can't determine it |
| 73 | + return 'main'; |
| 74 | + } catch { |
| 75 | + // Fallback to a sensible default |
| 76 | + return 'main'; |
| 77 | + } |
| 78 | +} |
| 79 | + |
| 80 | +// Get list of remote branches |
| 81 | +export function getRemoteBranches() { |
| 82 | + try { |
| 83 | + // Get all remote branches, excluding HEAD reference |
| 84 | + const output = execSync('git branch -r | grep -v HEAD').toString().trim(); |
| 85 | + |
| 86 | + // Parse and clean branch names |
| 87 | + return output.split('\n') |
| 88 | + .map(branch => branch.trim().replace(/^origin\//, '')) |
| 89 | + .filter(branch => branch !== ''); |
| 90 | + } catch { |
| 91 | + return []; |
| 92 | + } |
| 93 | +} |
| 94 | + |
57 | 95 | // Check if branch is pushed to remote |
58 | 96 | export function isBranchPushedToRemote(branchName) { |
59 | 97 | try { |
@@ -86,21 +124,62 @@ export function checkGhCli() { |
86 | 124 | } |
87 | 125 | } |
88 | 126 |
|
89 | | -// Get the template path |
| 127 | +// Default PR template content as a fallback |
| 128 | +const DEFAULT_TEMPLATE = `{% if has_ticket %} |
| 129 | +## Ticket |
| 130 | +{{ ticket_number }} |
| 131 | +{% endif %} |
| 132 | +
|
| 133 | +## Changes |
| 134 | +{% for change in changes %} |
| 135 | +- {{ change }} |
| 136 | +{% endfor %} |
| 137 | +
|
| 138 | +{% if has_tests %} |
| 139 | +## Tests |
| 140 | +- ✅ Includes tests |
| 141 | +{% else %} |
| 142 | +## Tests |
| 143 | +- ❌ No tests included |
| 144 | +{% endif %} |
| 145 | +`; |
| 146 | + |
| 147 | +// Get the directory where the script is installed |
| 148 | +function getScriptDir() { |
| 149 | + // Use import.meta.url to get the full URL of the current module |
| 150 | + const fileUrl = import.meta.url; |
| 151 | + // Convert the file URL to a system path and get the directory |
| 152 | + return path.dirname(new URL(fileUrl).pathname); |
| 153 | +} |
| 154 | + |
| 155 | +// Get the template path or create default template |
90 | 156 | export function getTemplatePath() { |
91 | | - const templatePath = path.join(__dirname, 'templates', 'PULL_REQUEST_TEMPLATE.twig'); |
| 157 | + // Get template ONLY from the script's installation directory |
| 158 | + const scriptDir = getScriptDir(); |
| 159 | + const templatePath = path.join(scriptDir, 'templates', 'PULL_REQUEST_TEMPLATE.twig'); |
92 | 160 |
|
| 161 | + // Check if template exists in the app installation directory |
93 | 162 | if (!existsSync(templatePath)) { |
94 | | - throw new Error('PR template not found: ' + templatePath); |
| 163 | + console.log(`🔍 Template not found in application directory: ${templatePath}`); |
| 164 | + console.log('⚠️ Using default template'); |
| 165 | + return { isDefault: true, content: DEFAULT_TEMPLATE }; |
95 | 166 | } |
96 | 167 |
|
97 | | - return templatePath; |
| 168 | + console.log(`📋 Using template from application directory: ${templatePath}`); |
| 169 | + return { isDefault: false, path: templatePath }; |
98 | 170 | } |
99 | 171 |
|
100 | 172 | // Create PR using GitHub CLI |
101 | | -export async function createPR(title, body) { |
| 173 | +export async function createPR(title, body, targetBranch = null) { |
102 | 174 | try { |
103 | | - const command = `gh pr create --title "${title}" --body "${body.replace(/"/g, '\\"')}"`; |
| 175 | + // Build the command with optional target branch |
| 176 | + let command = `gh pr create --title "${title}" --body "${body.replace(/"/g, '\\"')}"`; |
| 177 | + |
| 178 | + // Add target branch if specified |
| 179 | + if (targetBranch) { |
| 180 | + command += ` --base "${targetBranch}"`; |
| 181 | + } |
| 182 | + |
104 | 183 | const output = execSync(command).toString().trim(); |
105 | 184 | return { |
106 | 185 | success: true, |
@@ -179,15 +258,27 @@ export async function main() { |
179 | 258 | } |
180 | 259 | } |
181 | 260 |
|
182 | | - // Render template |
183 | | - const templatePath = getTemplatePath(); |
184 | | - |
185 | | - const renderedTemplate = await renderFileAsync(templatePath, { |
186 | | - ticket_number: ticketNumber || '', |
187 | | - changes, |
188 | | - has_tests: hasTests, |
189 | | - has_ticket: !!ticketNumber |
190 | | - }); |
| 261 | + // Get template and render it |
| 262 | + const template = getTemplatePath(); |
| 263 | + |
| 264 | + let renderedTemplate; |
| 265 | + if (template.isDefault) { |
| 266 | + // Render the default template string |
| 267 | + renderedTemplate = twig.twig({ data: template.content }).render({ |
| 268 | + ticket_number: ticketNumber || '', |
| 269 | + changes, |
| 270 | + has_tests: hasTests, |
| 271 | + has_ticket: !!ticketNumber |
| 272 | + }); |
| 273 | + } else { |
| 274 | + // Render from file |
| 275 | + renderedTemplate = await renderFileAsync(template.path, { |
| 276 | + ticket_number: ticketNumber || '', |
| 277 | + changes, |
| 278 | + has_tests: hasTests, |
| 279 | + has_ticket: !!ticketNumber |
| 280 | + }); |
| 281 | + } |
191 | 282 |
|
192 | 283 | console.log('\n📋 PR Preview:'); |
193 | 284 | console.log(`Title: ${ticketNumber ? `[${ticketNumber}] ` : ''}${prTitle}`); |
@@ -223,9 +314,26 @@ export async function main() { |
223 | 314 | console.log(`✅ Branch '${currentBranch}' successfully pushed to remote.`); |
224 | 315 | } |
225 | 316 |
|
226 | | - // Create PR |
| 317 | + const defaultBranch = getDefaultBranch(); |
| 318 | + // Default branch gets added to the start of the array. |
| 319 | + const remoteBranches = getRemoteBranches() |
| 320 | + .sort((branchA, branchB) => { |
| 321 | + return (branchA === defaultBranch) ? -1 : (branchB === defaultBranch) ? 1 : branchA.localeCompare(branchB); |
| 322 | + }) |
| 323 | + .map(branch => ({ title: branch, value: branch })); |
| 324 | + // Ask user which branch to target for PR |
| 325 | + // Default to the repository's default branch |
| 326 | + console.log('\n🌿 Select target branch for PR:'); |
| 327 | + const targetBranch = await search({ |
| 328 | + message: '🎯 Target branch for PR:', |
| 329 | + default: defaultBranch, |
| 330 | + source: (input = '') => { return remoteBranches.filter(branch => branch.title.includes(input)); }, |
| 331 | + }); |
| 332 | + |
| 333 | + console.log(`📌 Creating PR targeting branch: ${targetBranch}`); |
| 334 | + |
227 | 335 | const fullTitle = ticketNumber ? `[${ticketNumber}] ${prTitle}` : prTitle; |
228 | | - const result = await createPR(fullTitle, renderedTemplate); |
| 336 | + const result = await createPR(fullTitle, renderedTemplate, targetBranch); |
229 | 337 |
|
230 | 338 | if (result.success) { |
231 | 339 | console.log(`\n✅ Pull Request created successfully: ${result.url}`); |
|
0 commit comments