-
Notifications
You must be signed in to change notification settings - Fork 3
Expand file tree
/
Copy pathghpr-cli.js
More file actions
executable file
·251 lines (213 loc) · 7.62 KB
/
ghpr-cli.js
File metadata and controls
executable file
·251 lines (213 loc) · 7.62 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
#!/usr/bin/env node
const { execSync } = require('child_process');
const prompts = require('prompts');
// Colors for console output
const colors = {
reset: '\x1b[0m',
bright: '\x1b[1m',
red: '\x1b[31m',
green: '\x1b[32m',
yellow: '\x1b[33m',
blue: '\x1b[34m',
magenta: '\x1b[35m',
cyan: '\x1b[36m'
};
function log(message, color = 'reset') {
console.log(`${colors[color]}${message}${colors.reset}`);
}
function execCommand(command, options = {}) {
try {
const result = execSync(command, {
encoding: 'utf8',
stdio: options.silent ? 'pipe' : 'inherit',
...options
});
return result || '';
} catch (error) {
if (!options.silent) {
log(`Error executing command: ${command}`, 'red');
log(error.message, 'red');
}
throw error;
}
}
async function getGitHubCollaborators() {
try {
log('Fetching GitHub collaborators...', 'blue');
const output = execCommand('gh api repos/secoda/secoda/collaborators | jq -r \'.[].login\'', { silent: true });
return output.trim().split('\n').filter(username => username.length > 0);
} catch (error) {
log('Failed to fetch collaborators. Make sure you have GitHub CLI installed and are authenticated.', 'red');
log('You can install GitHub CLI with: brew install gh', 'yellow');
log('And authenticate with: gh auth login', 'yellow');
return [];
}
}
async function getCurrentBranch() {
try {
return execCommand('git rev-parse --abbrev-ref HEAD', { silent: true }).trim();
} catch (error) {
log('Not in a git repository', 'red');
process.exit(1);
}
}
async function checkExistingPR(branch) {
try {
const output = execCommand(`gh pr list --base main --head "${branch}" --json url,number,title`, { silent: true });
const prs = JSON.parse(output);
return prs.length > 0 ? prs[0] : null;
} catch (error) {
return null;
}
}
async function getPRUrl(branch) {
try {
const prQuery = `gh pr list --head "${branch}" --json url --jq '.[0].url'`;
const prUrlOutput = execCommand(prQuery, { silent: true });
const prUrl = prUrlOutput.trim();
return prUrl && prUrl !== 'null' ? prUrl : null;
} catch (error) {
return null;
}
}
async function createOrUpdatePR(title, reviewers, assignees, existingPR, currentBranch) {
try {
// Push the current branch
log('Pushing current branch...', 'blue');
execCommand('git push');
let prUrl;
if (existingPR) {
log(`Updating existing PR #${existingPR.number}...`, 'blue');
// Update the PR title
execCommand(`gh pr edit ${existingPR.number} --title "${title}"`, { silent: true });
// Add reviewers if any
if (reviewers.length > 0) {
execCommand(`gh pr edit ${existingPR.number} --add-reviewer ${reviewers.join(',')}`, { silent: true });
}
// Add assignees if any
if (assignees.length > 0) {
execCommand(`gh pr edit ${existingPR.number} --add-assignee ${assignees.join(',')}`, { silent: true });
}
// Get the PR URL
prUrl = await getPRUrl(currentBranch);
if (prUrl) {
log(`PR updated: ${prUrl}`, 'green');
} else {
log('Could not retrieve PR URL', 'yellow');
}
} else {
log('Creating new PR...', 'blue');
// Build the gh pr create command
let createCommand = `gh pr create --fill --title "${title}"`;
if (reviewers.length > 0) {
createCommand += ` --reviewer ${reviewers.join(',')}`;
}
if (assignees.length > 0) {
createCommand += ` --assignee ${assignees.join(',')}`;
}
// Create the PR (don't capture output as it might be empty)
execCommand(createCommand);
// Get the PR URL
prUrl = await getPRUrl(currentBranch);
if (prUrl) {
log(`PR created: ${prUrl}`, 'green');
} else {
log('PR created but could not retrieve URL', 'yellow');
}
}
return prUrl;
} catch (error) {
log('Failed to create/update PR', 'red');
log(error.message, 'red');
return null;
}
}
async function main() {
log('🚀 GitHub PR Assistant', 'bright');
log('====================', 'bright');
// Get current branch
const currentBranch = await getCurrentBranch();
log(`Current branch: ${currentBranch}`, 'cyan');
// Check for existing PR
const existingPR = await checkExistingPR(currentBranch);
if (existingPR) {
log(`Found existing PR: #${existingPR.number} - ${existingPR.title}`, 'yellow');
}
// Get GitHub collaborators
const collaborators = await getGitHubCollaborators();
if (collaborators.length === 0) {
log('No collaborators found. Proceeding without reviewer/assignee selection.', 'yellow');
}
// Interactive prompts
const questions = [
{
type: 'text',
name: 'prefix',
message: 'Enter a prefix for the PR title (e.g., fix, feat, docs):',
initial: existingPR ? existingPR.title.split(':')[0] : 'fix',
validate: value => value.trim().length > 0 ? true : 'Prefix is required'
}
];
// Add collaborator selection if we have collaborators
if (collaborators.length > 0) {
questions.push(
{
type: 'multiselect',
name: 'reviewers',
message: 'Select reviewers (use space to select, enter to confirm):',
choices: collaborators.map(username => ({
title: username,
value: username,
selected: false
})),
instructions: false
},
// {
// type: 'multiselect',
// name: 'assignees',
// message: 'Select assignees (use space to select, enter to confirm):',
// choices: collaborators.map(username => ({
// title: username,
// value: username,
// selected: false
// })),
// instructions: false
// }
);
}
const response = await prompts(questions);
if (!response.prefix) {
log('Operation cancelled', 'yellow');
process.exit(0);
}
// Generate PR title
const title = `${response.prefix}: ${currentBranch}`;
log(`\nGenerated PR title: ${title}`, 'cyan');
// Create or update PR
const reviewers = response.reviewers || [];
const assignees = response.assignees || [];
if (reviewers.length > 0) {
log(`Reviewers: ${reviewers.join(', ')}`, 'blue');
}
if (assignees.length > 0) {
log(`Assignees: ${assignees.join(', ')}`, 'blue');
}
const prUrl = await createOrUpdatePR(title, reviewers, assignees, existingPR, currentBranch);
if (prUrl) {
log('\n📋 Copy and paste the following in Slack:', 'bright');
log(`[${title}](${prUrl})`, 'green');
} else {
log('Failed to create/update PR', 'red');
process.exit(1);
}
}
// Handle Ctrl+C gracefully
process.on('SIGINT', () => {
log('\n\nOperation cancelled by user', 'yellow');
process.exit(0);
});
// Run the main function
main().catch(error => {
log(`\nUnexpected error: ${error.message}`, 'red');
process.exit(1);
});