claude-git-hooks 2.10.0 → 2.12.0
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/CHANGELOG.md +94 -7
- package/README.md +76 -1
- package/bin/claude-hooks +8 -0
- package/lib/commands/bump-version.js +452 -0
- package/lib/commands/create-pr.js +201 -1
- package/lib/commands/generate-changelog.js +154 -0
- package/lib/commands/help.js +53 -0
- package/lib/config.js +5 -0
- package/lib/utils/changelog-generator.js +382 -0
- package/lib/utils/git-operations.js +214 -1
- package/lib/utils/git-tag-manager.js +516 -0
- package/lib/utils/version-manager.js +583 -0
- package/package.json +1 -1
- package/templates/GENERATE_CHANGELOG.md +83 -0
|
@@ -0,0 +1,452 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File: bump-version.js
|
|
3
|
+
* Purpose: Bump version command with automatic CHANGELOG and Git tag
|
|
4
|
+
*
|
|
5
|
+
* Workflow:
|
|
6
|
+
* 1. Validate prerequisites (clean working directory, valid branch, remote)
|
|
7
|
+
* 2. Detect project type and current version
|
|
8
|
+
* 3. Calculate new version with optional suffix
|
|
9
|
+
* 4. Update version file(s)
|
|
10
|
+
* 5. [Optional] Generate and update CHANGELOG
|
|
11
|
+
* 6. Create annotated Git tag
|
|
12
|
+
* 7. Push tag to remote
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { execSync } from 'child_process';
|
|
16
|
+
import fs from 'fs';
|
|
17
|
+
import path from 'path';
|
|
18
|
+
import {
|
|
19
|
+
detectProjectType,
|
|
20
|
+
getCurrentVersion,
|
|
21
|
+
incrementVersion,
|
|
22
|
+
updateVersion,
|
|
23
|
+
parseVersion
|
|
24
|
+
} from '../utils/version-manager.js';
|
|
25
|
+
import {
|
|
26
|
+
createTag,
|
|
27
|
+
pushTags,
|
|
28
|
+
tagExists,
|
|
29
|
+
formatTagName
|
|
30
|
+
} from '../utils/git-tag-manager.js';
|
|
31
|
+
import {
|
|
32
|
+
generateChangelogEntry,
|
|
33
|
+
updateChangelogFile
|
|
34
|
+
} from '../utils/changelog-generator.js';
|
|
35
|
+
import {
|
|
36
|
+
getRepoRoot,
|
|
37
|
+
getCurrentBranch,
|
|
38
|
+
verifyRemoteExists,
|
|
39
|
+
getRemoteName
|
|
40
|
+
} from '../utils/git-operations.js';
|
|
41
|
+
import { getConfig } from '../config.js';
|
|
42
|
+
import { showInfo, showSuccess, showError, showWarning, promptConfirmation } from '../utils/interactive-ui.js';
|
|
43
|
+
import logger from '../utils/logger.js';
|
|
44
|
+
import {
|
|
45
|
+
colors,
|
|
46
|
+
error,
|
|
47
|
+
success,
|
|
48
|
+
info,
|
|
49
|
+
warning,
|
|
50
|
+
checkGitRepo
|
|
51
|
+
} from './helpers.js';
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Validates prerequisites before version bump
|
|
55
|
+
* Why: Prevents version bump in invalid state
|
|
56
|
+
*
|
|
57
|
+
* @returns {Object} Validation result: { valid, errors }
|
|
58
|
+
*/
|
|
59
|
+
function validatePrerequisites() {
|
|
60
|
+
logger.debug('bump-version - validatePrerequisites', 'Validating prerequisites');
|
|
61
|
+
|
|
62
|
+
const errors = [];
|
|
63
|
+
|
|
64
|
+
try {
|
|
65
|
+
// Check if git repo
|
|
66
|
+
if (!checkGitRepo()) {
|
|
67
|
+
errors.push('Not a git repository');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Check for clean working directory
|
|
71
|
+
try {
|
|
72
|
+
const status = execSync('git status --porcelain', { encoding: 'utf8' }).trim();
|
|
73
|
+
if (status) {
|
|
74
|
+
errors.push('Working directory has uncommitted changes');
|
|
75
|
+
}
|
|
76
|
+
} catch (err) {
|
|
77
|
+
errors.push('Failed to check git status');
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Check for valid branch (not detached HEAD)
|
|
81
|
+
const currentBranch = getCurrentBranch();
|
|
82
|
+
if (!currentBranch || currentBranch === 'unknown') {
|
|
83
|
+
errors.push('Not on a valid branch (detached HEAD?)');
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Check for remote
|
|
87
|
+
const remoteName = getRemoteName();
|
|
88
|
+
if (!verifyRemoteExists(remoteName)) {
|
|
89
|
+
errors.push(`Remote '${remoteName}' does not exist`);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const valid = errors.length === 0;
|
|
93
|
+
|
|
94
|
+
logger.debug('bump-version - validatePrerequisites', 'Validation complete', {
|
|
95
|
+
valid,
|
|
96
|
+
errorCount: errors.length
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
return { valid, errors };
|
|
100
|
+
|
|
101
|
+
} catch (err) {
|
|
102
|
+
logger.error('bump-version - validatePrerequisites', 'Validation failed', err);
|
|
103
|
+
return {
|
|
104
|
+
valid: false,
|
|
105
|
+
errors: ['Validation error: ' + err.message]
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Parses command line arguments
|
|
112
|
+
* Why: Extracts bump type and options from CLI args
|
|
113
|
+
*
|
|
114
|
+
* @param {Array<string>} args - CLI arguments
|
|
115
|
+
* @returns {Object} Parsed args: { bumpType, suffix, updateChangelog, baseBranch, dryRun, noTag, noPush }
|
|
116
|
+
*/
|
|
117
|
+
function parseArguments(args) {
|
|
118
|
+
logger.debug('bump-version - parseArguments', 'Parsing arguments', { args });
|
|
119
|
+
|
|
120
|
+
const parsed = {
|
|
121
|
+
bumpType: null,
|
|
122
|
+
suffix: null,
|
|
123
|
+
updateChangelog: false,
|
|
124
|
+
baseBranch: 'main',
|
|
125
|
+
dryRun: false,
|
|
126
|
+
noTag: false,
|
|
127
|
+
noPush: false
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
// First argument should be bump type
|
|
131
|
+
if (args.length === 0) {
|
|
132
|
+
return parsed;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const firstArg = args[0].toLowerCase();
|
|
136
|
+
if (['major', 'minor', 'patch'].includes(firstArg)) {
|
|
137
|
+
parsed.bumpType = firstArg;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Parse options
|
|
141
|
+
for (let i = 1; i < args.length; i++) {
|
|
142
|
+
const arg = args[i];
|
|
143
|
+
|
|
144
|
+
if (arg === '--suffix' && i + 1 < args.length) {
|
|
145
|
+
parsed.suffix = args[i + 1];
|
|
146
|
+
i++; // Skip next arg
|
|
147
|
+
} else if (arg === '--update-changelog') {
|
|
148
|
+
parsed.updateChangelog = true;
|
|
149
|
+
// Check if next arg is a branch name (not starting with --)
|
|
150
|
+
if (i + 1 < args.length && !args[i + 1].startsWith('--')) {
|
|
151
|
+
parsed.baseBranch = args[i + 1];
|
|
152
|
+
i++; // Skip next arg
|
|
153
|
+
}
|
|
154
|
+
} else if (arg === '--dry-run') {
|
|
155
|
+
parsed.dryRun = true;
|
|
156
|
+
} else if (arg === '--no-tag') {
|
|
157
|
+
parsed.noTag = true;
|
|
158
|
+
} else if (arg === '--no-push') {
|
|
159
|
+
parsed.noPush = true;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
logger.debug('bump-version - parseArguments', 'Arguments parsed', parsed);
|
|
164
|
+
|
|
165
|
+
return parsed;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Displays dry run preview
|
|
170
|
+
* Why: Shows what would change without applying
|
|
171
|
+
*
|
|
172
|
+
* @param {Object} info - Version info
|
|
173
|
+
*/
|
|
174
|
+
function showDryRunPreview(info) {
|
|
175
|
+
console.log('');
|
|
176
|
+
console.log(`${colors.yellow}═════════════════════════════════════════════════${colors.reset}`);
|
|
177
|
+
console.log(`${colors.yellow} DRY RUN - No changes will be made ${colors.reset}`);
|
|
178
|
+
console.log(`${colors.yellow}═════════════════════════════════════════════════${colors.reset}`);
|
|
179
|
+
console.log('');
|
|
180
|
+
console.log(`${colors.blue}Project Type:${colors.reset} ${info.projectType}`);
|
|
181
|
+
console.log(`${colors.blue}Current Version:${colors.reset} ${info.currentVersion}`);
|
|
182
|
+
console.log(`${colors.green}New Version:${colors.reset} ${info.newVersion}`);
|
|
183
|
+
console.log(`${colors.blue}Tag Name:${colors.reset} ${info.tagName}`);
|
|
184
|
+
console.log('');
|
|
185
|
+
|
|
186
|
+
if (info.files.length > 0) {
|
|
187
|
+
console.log(`${colors.blue}Files to update:${colors.reset}`);
|
|
188
|
+
info.files.forEach(file => console.log(` - ${file}`));
|
|
189
|
+
console.log('');
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (info.updateChangelog) {
|
|
193
|
+
console.log(`${colors.blue}CHANGELOG:${colors.reset} Will be generated and updated`);
|
|
194
|
+
console.log('');
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
console.log(`${colors.yellow}Run without --dry-run to apply these changes${colors.reset}`);
|
|
198
|
+
console.log('');
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Bump version command
|
|
203
|
+
* @param {Array<string>} args - Command arguments
|
|
204
|
+
*/
|
|
205
|
+
export async function runBumpVersion(args) {
|
|
206
|
+
logger.debug('bump-version', 'Starting bump-version command', { args });
|
|
207
|
+
|
|
208
|
+
// Parse arguments
|
|
209
|
+
const options = parseArguments(args);
|
|
210
|
+
|
|
211
|
+
if (!options.bumpType) {
|
|
212
|
+
error('Usage: claude-hooks bump-version <major|minor|patch> [options]');
|
|
213
|
+
console.log('');
|
|
214
|
+
console.log('Options:');
|
|
215
|
+
console.log(' --suffix <value> Add version suffix (e.g., SNAPSHOT, RC)');
|
|
216
|
+
console.log(' --update-changelog [branch] Generate CHANGELOG entry (default branch: main)');
|
|
217
|
+
console.log(' --dry-run Preview changes without applying');
|
|
218
|
+
console.log(' --no-tag Skip Git tag creation');
|
|
219
|
+
console.log(' --no-push Create tag but don\'t push to remote');
|
|
220
|
+
console.log('');
|
|
221
|
+
console.log('Examples:');
|
|
222
|
+
console.log(' claude-hooks bump-version minor --suffix SNAPSHOT');
|
|
223
|
+
console.log(' claude-hooks bump-version patch --update-changelog');
|
|
224
|
+
console.log(' claude-hooks bump-version major --dry-run');
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
showInfo(`🔢 Bumping version (${options.bumpType})...`);
|
|
229
|
+
console.log('');
|
|
230
|
+
|
|
231
|
+
// Step 1: Validate prerequisites
|
|
232
|
+
logger.debug('bump-version', 'Step 1: Validating prerequisites');
|
|
233
|
+
const validation = validatePrerequisites();
|
|
234
|
+
|
|
235
|
+
if (!validation.valid) {
|
|
236
|
+
showError('Prerequisites validation failed:');
|
|
237
|
+
console.log('');
|
|
238
|
+
validation.errors.forEach(err => console.log(` ❌ ${err}`));
|
|
239
|
+
console.log('');
|
|
240
|
+
console.log('Please fix these issues:');
|
|
241
|
+
console.log(' - Commit or stash uncommitted changes: git stash');
|
|
242
|
+
console.log(' - Checkout a branch: git checkout main');
|
|
243
|
+
console.log(' - Configure remote: git remote add origin <url>');
|
|
244
|
+
console.log('');
|
|
245
|
+
process.exit(1);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
showSuccess('Prerequisites validated');
|
|
249
|
+
|
|
250
|
+
// Step 2: Detect project type and current version
|
|
251
|
+
logger.debug('bump-version', 'Step 2: Detecting project type and version');
|
|
252
|
+
const projectType = detectProjectType();
|
|
253
|
+
|
|
254
|
+
if (projectType === 'none') {
|
|
255
|
+
showError('No version files found');
|
|
256
|
+
console.log('');
|
|
257
|
+
console.log('This command requires either:');
|
|
258
|
+
console.log(' - package.json (Node.js project)');
|
|
259
|
+
console.log(' - pom.xml (Maven project)');
|
|
260
|
+
console.log('');
|
|
261
|
+
process.exit(1);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const versions = getCurrentVersion(projectType);
|
|
265
|
+
const currentVersion = versions.resolved;
|
|
266
|
+
|
|
267
|
+
if (!currentVersion) {
|
|
268
|
+
showError('Could not determine current version');
|
|
269
|
+
process.exit(1);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Warn if monorepo has version mismatch
|
|
273
|
+
if (versions.mismatch) {
|
|
274
|
+
showWarning('Version mismatch detected in monorepo:');
|
|
275
|
+
console.log(` package.json: ${versions.packageJson}`);
|
|
276
|
+
console.log(` pom.xml: ${versions.pomXml}`);
|
|
277
|
+
console.log('');
|
|
278
|
+
console.log('Both files will be updated to the new version.');
|
|
279
|
+
console.log('');
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
showInfo(`Project: ${projectType}`);
|
|
283
|
+
showInfo(`Current version: ${currentVersion}`);
|
|
284
|
+
|
|
285
|
+
// Step 3: Calculate new version
|
|
286
|
+
logger.debug('bump-version', 'Step 3: Calculating new version');
|
|
287
|
+
const newVersion = incrementVersion(currentVersion, options.bumpType, options.suffix);
|
|
288
|
+
const tagName = formatTagName(newVersion);
|
|
289
|
+
|
|
290
|
+
showSuccess(`New version: ${newVersion}`);
|
|
291
|
+
console.log('');
|
|
292
|
+
|
|
293
|
+
// Prepare files list
|
|
294
|
+
const filesToUpdate = [];
|
|
295
|
+
if (projectType === 'node' || projectType === 'both') {
|
|
296
|
+
filesToUpdate.push('package.json');
|
|
297
|
+
}
|
|
298
|
+
if (projectType === 'maven' || projectType === 'both') {
|
|
299
|
+
filesToUpdate.push('pom.xml');
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// Dry run preview
|
|
303
|
+
if (options.dryRun) {
|
|
304
|
+
showDryRunPreview({
|
|
305
|
+
projectType,
|
|
306
|
+
currentVersion,
|
|
307
|
+
newVersion,
|
|
308
|
+
tagName,
|
|
309
|
+
files: filesToUpdate,
|
|
310
|
+
updateChangelog: options.updateChangelog
|
|
311
|
+
});
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// Confirm with user
|
|
316
|
+
const shouldContinue = await promptConfirmation(
|
|
317
|
+
`Update version to ${newVersion}?`,
|
|
318
|
+
true
|
|
319
|
+
);
|
|
320
|
+
|
|
321
|
+
if (!shouldContinue) {
|
|
322
|
+
showInfo('Version bump cancelled');
|
|
323
|
+
process.exit(0);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
console.log('');
|
|
327
|
+
|
|
328
|
+
try {
|
|
329
|
+
// Step 4: Update version files
|
|
330
|
+
logger.debug('bump-version', 'Step 4: Updating version files');
|
|
331
|
+
showInfo('Updating version files...');
|
|
332
|
+
updateVersion(projectType, newVersion);
|
|
333
|
+
|
|
334
|
+
filesToUpdate.forEach(file => {
|
|
335
|
+
showSuccess(`✓ Updated ${file}`);
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
console.log('');
|
|
339
|
+
|
|
340
|
+
// Step 5: Generate CHANGELOG (if requested)
|
|
341
|
+
if (options.updateChangelog) {
|
|
342
|
+
logger.debug('bump-version', 'Step 5: Generating CHANGELOG');
|
|
343
|
+
showInfo('Generating CHANGELOG entry...');
|
|
344
|
+
|
|
345
|
+
const config = await getConfig();
|
|
346
|
+
const isReleaseVersion = !options.suffix; // Final version if no suffix
|
|
347
|
+
|
|
348
|
+
const changelogResult = await generateChangelogEntry({
|
|
349
|
+
version: newVersion,
|
|
350
|
+
isReleaseVersion,
|
|
351
|
+
baseBranch: options.baseBranch,
|
|
352
|
+
config
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
if (changelogResult.content) {
|
|
356
|
+
const updated = updateChangelogFile(changelogResult.content);
|
|
357
|
+
if (updated) {
|
|
358
|
+
showSuccess('✓ CHANGELOG.md updated');
|
|
359
|
+
} else {
|
|
360
|
+
showWarning('⚠ Failed to update CHANGELOG.md');
|
|
361
|
+
}
|
|
362
|
+
} else {
|
|
363
|
+
showWarning('⚠ No commits found for CHANGELOG');
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
console.log('');
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// Step 6: Create Git tag (if not disabled)
|
|
370
|
+
if (!options.noTag) {
|
|
371
|
+
logger.debug('bump-version', 'Step 6: Creating Git tag');
|
|
372
|
+
showInfo('Creating Git tag...');
|
|
373
|
+
|
|
374
|
+
// Check if tag already exists
|
|
375
|
+
const exists = await tagExists(tagName, 'local');
|
|
376
|
+
if (exists) {
|
|
377
|
+
showWarning(`Tag ${tagName} already exists locally`);
|
|
378
|
+
const shouldOverwrite = await promptConfirmation(
|
|
379
|
+
'Overwrite existing tag?',
|
|
380
|
+
false
|
|
381
|
+
);
|
|
382
|
+
|
|
383
|
+
if (!shouldOverwrite) {
|
|
384
|
+
showInfo('Tag creation skipped');
|
|
385
|
+
console.log('');
|
|
386
|
+
console.log('Version files updated successfully, but tag was not created.');
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
const tagMessage = `Release version ${newVersion}`;
|
|
392
|
+
const tagResult = createTag(newVersion, tagMessage, { force: exists });
|
|
393
|
+
|
|
394
|
+
if (tagResult.success) {
|
|
395
|
+
showSuccess(`✓ Tag created: ${tagName}`);
|
|
396
|
+
console.log('');
|
|
397
|
+
|
|
398
|
+
// Step 7: Push tag (if not disabled)
|
|
399
|
+
if (!options.noPush) {
|
|
400
|
+
logger.debug('bump-version', 'Step 7: Pushing tag to remote');
|
|
401
|
+
showInfo('Pushing tag to remote...');
|
|
402
|
+
|
|
403
|
+
const pushResult = pushTags(null, tagName);
|
|
404
|
+
|
|
405
|
+
if (pushResult.success) {
|
|
406
|
+
showSuccess(`✓ Tag pushed to remote`);
|
|
407
|
+
} else {
|
|
408
|
+
showError(`Failed to push tag: ${pushResult.error}`);
|
|
409
|
+
console.log('');
|
|
410
|
+
console.log('You can push the tag manually:');
|
|
411
|
+
console.log(` git push origin ${tagName}`);
|
|
412
|
+
}
|
|
413
|
+
} else {
|
|
414
|
+
showInfo('Tag push skipped (--no-push)');
|
|
415
|
+
console.log('');
|
|
416
|
+
console.log('To push the tag later:');
|
|
417
|
+
console.log(` git push origin ${tagName}`);
|
|
418
|
+
}
|
|
419
|
+
} else {
|
|
420
|
+
showError(`Failed to create tag: ${tagResult.error}`);
|
|
421
|
+
}
|
|
422
|
+
} else {
|
|
423
|
+
showInfo('Tag creation skipped (--no-tag)');
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// Success summary
|
|
427
|
+
console.log('');
|
|
428
|
+
console.log(`${colors.green}════════════════════════════════════════════════${colors.reset}`);
|
|
429
|
+
console.log(`${colors.green} Version Bump Complete ✅ ${colors.reset}`);
|
|
430
|
+
console.log(`${colors.green}════════════════════════════════════════════════${colors.reset}`);
|
|
431
|
+
console.log('');
|
|
432
|
+
console.log(`${colors.blue}Old version:${colors.reset} ${currentVersion}`);
|
|
433
|
+
console.log(`${colors.blue}New version:${colors.reset} ${newVersion}`);
|
|
434
|
+
console.log(`${colors.blue}Tag:${colors.reset} ${tagName}`);
|
|
435
|
+
console.log('');
|
|
436
|
+
console.log('Next steps:');
|
|
437
|
+
console.log(' 1. Review the changes: git diff');
|
|
438
|
+
console.log(' 2. Commit the version bump: git add . && git commit -m "chore: bump version to ' + newVersion + '"');
|
|
439
|
+
console.log(' 3. Create PR: claude-hooks create-pr main');
|
|
440
|
+
console.log('');
|
|
441
|
+
|
|
442
|
+
} catch (err) {
|
|
443
|
+
logger.error('bump-version', 'Version bump failed', err);
|
|
444
|
+
showError(`Version bump failed: ${err.message}`);
|
|
445
|
+
console.log('');
|
|
446
|
+
console.log('The operation was interrupted. You may need to:');
|
|
447
|
+
console.log(' - Revert changes: git checkout .');
|
|
448
|
+
console.log(' - Delete tag if created: git tag -d ' + tagName);
|
|
449
|
+
console.log('');
|
|
450
|
+
process.exit(1);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
@@ -11,7 +11,8 @@ import { loadPrompt } from '../utils/prompt-builder.js';
|
|
|
11
11
|
import { getConfig } from '../config.js';
|
|
12
12
|
import { getOrPromptTaskId, formatWithTaskId } from '../utils/task-id.js';
|
|
13
13
|
import { getReviewersForFiles } from '../utils/github-client.js';
|
|
14
|
-
import { showPRPreview, promptMenu, showSuccess, showError, showInfo, showWarning } from '../utils/interactive-ui.js';
|
|
14
|
+
import { showPRPreview, promptMenu, showSuccess, showError, showInfo, showWarning, promptConfirmation } from '../utils/interactive-ui.js';
|
|
15
|
+
import { getBranchPushStatus, pushBranch } from '../utils/git-operations.js';
|
|
15
16
|
import logger from '../utils/logger.js';
|
|
16
17
|
import {
|
|
17
18
|
error,
|
|
@@ -136,6 +137,205 @@ export async function runCreatePr(args) {
|
|
|
136
137
|
|
|
137
138
|
logger.debug('create-pr', 'No existing PR found, continuing');
|
|
138
139
|
|
|
140
|
+
// Step 5.5: Check branch status and auto-push if needed (v2.11.0)
|
|
141
|
+
logger.debug('create-pr', 'Step 5.5: Checking branch push status');
|
|
142
|
+
const pushStatus = getBranchPushStatus(currentBranch);
|
|
143
|
+
const pushConfig = config.github?.pr;
|
|
144
|
+
|
|
145
|
+
logger.debug('create-pr', 'Branch push status', {
|
|
146
|
+
hasRemote: pushStatus.hasRemote,
|
|
147
|
+
hasUnpushedCommits: pushStatus.hasUnpushedCommits,
|
|
148
|
+
hasDiverged: pushStatus.hasDiverged,
|
|
149
|
+
unpushedCount: pushStatus.unpushedCommits.length
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
if (!pushStatus.hasRemote || pushStatus.hasUnpushedCommits) {
|
|
153
|
+
// Branch needs to be pushed
|
|
154
|
+
|
|
155
|
+
if (pushStatus.hasDiverged) {
|
|
156
|
+
// ABORT: Cannot push diverged branch
|
|
157
|
+
logger.error('create-pr', 'Branch has diverged from remote', {
|
|
158
|
+
branch: currentBranch,
|
|
159
|
+
upstream: pushStatus.upstreamBranch
|
|
160
|
+
});
|
|
161
|
+
showError('Branch has diverged from remote');
|
|
162
|
+
console.log('');
|
|
163
|
+
console.log('Please resolve conflicts manually:');
|
|
164
|
+
console.log(` git pull origin ${currentBranch}`);
|
|
165
|
+
console.log('');
|
|
166
|
+
process.exit(1);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (!pushConfig?.autoPush) {
|
|
170
|
+
// Auto-push disabled - show error
|
|
171
|
+
logger.debug('create-pr', 'Auto-push disabled, aborting', { autoPush: pushConfig?.autoPush });
|
|
172
|
+
showError('Branch not pushed to remote');
|
|
173
|
+
console.log('');
|
|
174
|
+
console.log('Please push your branch first:');
|
|
175
|
+
console.log(` git push -u origin ${currentBranch}`);
|
|
176
|
+
console.log('');
|
|
177
|
+
process.exit(1);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Show push preview
|
|
181
|
+
if (pushConfig?.showCommits && pushStatus.unpushedCommits.length > 0) {
|
|
182
|
+
console.log('');
|
|
183
|
+
showInfo('Commits to push:');
|
|
184
|
+
pushStatus.unpushedCommits.forEach(commit => {
|
|
185
|
+
console.log(` ${commit.sha.substring(0, 7)} ${commit.message}`);
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Confirm with user
|
|
190
|
+
if (pushConfig?.pushConfirm) {
|
|
191
|
+
console.log('');
|
|
192
|
+
const shouldPush = await promptConfirmation(
|
|
193
|
+
`Push branch '${currentBranch}' to remote?`,
|
|
194
|
+
true // default yes
|
|
195
|
+
);
|
|
196
|
+
|
|
197
|
+
if (!shouldPush) {
|
|
198
|
+
logger.debug('create-pr', 'User declined push, aborting');
|
|
199
|
+
showInfo('Push cancelled - cannot create PR without pushing');
|
|
200
|
+
process.exit(0);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Execute push
|
|
205
|
+
console.log('');
|
|
206
|
+
showInfo('Pushing branch to remote...');
|
|
207
|
+
logger.debug('create-pr', 'Executing push', {
|
|
208
|
+
branch: currentBranch,
|
|
209
|
+
setUpstream: !pushStatus.hasRemote
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
const pushResult = pushBranch(currentBranch, {
|
|
213
|
+
setUpstream: !pushStatus.hasRemote
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
if (!pushResult.success) {
|
|
217
|
+
logger.error('create-pr', 'Push failed', {
|
|
218
|
+
branch: currentBranch,
|
|
219
|
+
error: pushResult.error
|
|
220
|
+
});
|
|
221
|
+
showError('Failed to push branch');
|
|
222
|
+
console.error('');
|
|
223
|
+
console.error(` ${pushResult.error}`);
|
|
224
|
+
console.error('');
|
|
225
|
+
process.exit(1);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
logger.debug('create-pr', 'Push completed successfully');
|
|
229
|
+
showSuccess('Branch pushed successfully');
|
|
230
|
+
console.log('');
|
|
231
|
+
} else {
|
|
232
|
+
logger.debug('create-pr', 'Branch already up-to-date with remote, skipping push');
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Step 5.6: Version alignment validation (Issue #44)
|
|
236
|
+
logger.debug('create-pr', 'Step 5.6: Validating version alignment');
|
|
237
|
+
const { validateVersionAlignment } = await import('../utils/version-manager.js');
|
|
238
|
+
const versionCheck = await validateVersionAlignment();
|
|
239
|
+
|
|
240
|
+
if (!versionCheck.aligned) {
|
|
241
|
+
showWarning('Version misalignment detected:');
|
|
242
|
+
console.log('');
|
|
243
|
+
|
|
244
|
+
for (const issue of versionCheck.issues) {
|
|
245
|
+
console.log(` ${issue.source}: ${issue.version || 'not found'}`);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
console.log('');
|
|
249
|
+
console.log('To fix version alignment:');
|
|
250
|
+
console.log(' claude-hooks bump-version patch --update-changelog');
|
|
251
|
+
console.log('');
|
|
252
|
+
|
|
253
|
+
const shouldContinue = await promptConfirmation(
|
|
254
|
+
'Continue creating PR despite version misalignment?',
|
|
255
|
+
false // default no
|
|
256
|
+
);
|
|
257
|
+
|
|
258
|
+
if (!shouldContinue) {
|
|
259
|
+
showInfo('PR creation cancelled - please align versions first');
|
|
260
|
+
process.exit(0);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
if (versionCheck.aligned && versionCheck.comparison === 'equal') {
|
|
265
|
+
showWarning('Local version equals remote version');
|
|
266
|
+
console.log(` Local: ${versionCheck.localVersion}`);
|
|
267
|
+
console.log(` Remote: ${versionCheck.remoteVersion}`);
|
|
268
|
+
console.log('');
|
|
269
|
+
console.log('To bump version:');
|
|
270
|
+
console.log(' claude-hooks bump-version patch # or minor/major');
|
|
271
|
+
console.log('');
|
|
272
|
+
|
|
273
|
+
const shouldContinue = await promptConfirmation(
|
|
274
|
+
'Continue creating PR without version bump?',
|
|
275
|
+
true // default yes (might be non-release PR)
|
|
276
|
+
);
|
|
277
|
+
|
|
278
|
+
if (!shouldContinue) {
|
|
279
|
+
showInfo('PR creation cancelled');
|
|
280
|
+
process.exit(0);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
if (versionCheck.aligned && versionCheck.comparison === 'less') {
|
|
285
|
+
showError('Local version is less than remote version!');
|
|
286
|
+
console.log(` Local: ${versionCheck.localVersion}`);
|
|
287
|
+
console.log(` Remote: ${versionCheck.remoteVersion}`);
|
|
288
|
+
console.log('');
|
|
289
|
+
console.log('This usually means you need to pull latest changes:');
|
|
290
|
+
console.log(` git pull origin ${baseBranch}`);
|
|
291
|
+
console.log('');
|
|
292
|
+
|
|
293
|
+
const shouldContinue = await promptConfirmation(
|
|
294
|
+
'Continue creating PR despite version being behind?',
|
|
295
|
+
false // default no
|
|
296
|
+
);
|
|
297
|
+
|
|
298
|
+
if (!shouldContinue) {
|
|
299
|
+
showInfo('PR creation cancelled');
|
|
300
|
+
process.exit(0);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// Step 5.7: Check for unpushed tags (Issue #44)
|
|
305
|
+
logger.debug('create-pr', 'Step 5.7: Checking for unpushed tags');
|
|
306
|
+
const { compareLocalAndRemoteTags, pushTags: pushTagsUtil } = await import('../utils/git-tag-manager.js');
|
|
307
|
+
const tagComparison = await compareLocalAndRemoteTags();
|
|
308
|
+
|
|
309
|
+
if (tagComparison.localNewer.length > 0) {
|
|
310
|
+
showWarning(`Local tags not pushed: ${tagComparison.localNewer.join(', ')}`);
|
|
311
|
+
console.log('');
|
|
312
|
+
|
|
313
|
+
const shouldPushTags = await promptConfirmation(
|
|
314
|
+
'Push tags to remote?',
|
|
315
|
+
true // default yes
|
|
316
|
+
);
|
|
317
|
+
|
|
318
|
+
if (shouldPushTags) {
|
|
319
|
+
showInfo('Pushing tags...');
|
|
320
|
+
const pushResult = pushTagsUtil(null, tagComparison.localNewer);
|
|
321
|
+
|
|
322
|
+
if (pushResult.success) {
|
|
323
|
+
showSuccess('Tags pushed successfully');
|
|
324
|
+
} else {
|
|
325
|
+
showError(`Failed to push some tags: ${pushResult.error}`);
|
|
326
|
+
if (pushResult.failed.length > 0) {
|
|
327
|
+
console.log('Failed tags:');
|
|
328
|
+
pushResult.failed.forEach(f => console.log(` - ${f.tag}: ${f.error}`));
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
console.log('');
|
|
333
|
+
} else {
|
|
334
|
+
showInfo('Tag push skipped');
|
|
335
|
+
console.log('');
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
139
339
|
// Step 6: Update remote and check for differences
|
|
140
340
|
logger.debug('create-pr', 'Step 6: Fetching latest changes from remote');
|
|
141
341
|
execSync('git fetch', { stdio: 'ignore' });
|