claude-git-hooks 2.10.0 → 2.11.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 +33 -0
- package/README.md +17 -1
- package/lib/commands/create-pr.js +97 -1
- package/lib/config.js +5 -0
- package/lib/utils/git-operations.js +214 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,39 @@ Todos los cambios notables en este proyecto se documentarán en este archivo.
|
|
|
5
5
|
El formato está basado en [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
y este proyecto adhiere a [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [2.11.0] - 2026-02-03
|
|
9
|
+
|
|
10
|
+
### ✨ Added
|
|
11
|
+
|
|
12
|
+
- **Auto-push functionality in `create-pr` command** - Automatically detects and pushes unpublished branches before creating PR (#59, #34)
|
|
13
|
+
- Handles 3 scenarios: unpublished branch, unpushed commits, up-to-date branch
|
|
14
|
+
- Interactive confirmation prompt before pushing (configurable)
|
|
15
|
+
- Commit preview showing what will be pushed
|
|
16
|
+
- Diverged branch detection with helpful error message and resolution steps
|
|
17
|
+
- Never uses `git push --force` for security
|
|
18
|
+
|
|
19
|
+
- **New configuration options** under `github.pr`:
|
|
20
|
+
- `autoPush: true` - Enable/disable auto-push (enabled by default)
|
|
21
|
+
- `pushConfirm: true` - Prompt for confirmation before push
|
|
22
|
+
- `showCommits: true` - Show commit preview before push
|
|
23
|
+
- `verifyRemote: true` - Verify remote exists before push
|
|
24
|
+
|
|
25
|
+
- **New git-operations functions**:
|
|
26
|
+
- `getRemoteName()` - Get configured remote name (usually 'origin')
|
|
27
|
+
- `verifyRemoteExists()` - Check if remote is configured
|
|
28
|
+
- `getBranchPushStatus()` - Detect branch publish status and unpushed commits
|
|
29
|
+
- `pushBranch()` - Execute git push with upstream tracking
|
|
30
|
+
|
|
31
|
+
### 🔧 Fixed
|
|
32
|
+
|
|
33
|
+
- Issue #59: `create-pr` no longer fails when branch is not published to remote
|
|
34
|
+
- Issue #34: Git push now integrated into create-pr workflow with proper error handling
|
|
35
|
+
|
|
36
|
+
### 📚 Documentation
|
|
37
|
+
|
|
38
|
+
- Updated CLAUDE.md with new git-operations functions and configuration options
|
|
39
|
+
- Added comprehensive JSDoc comments for all new functions
|
|
40
|
+
|
|
8
41
|
## [2.10.0] - 2026-02-02
|
|
9
42
|
|
|
10
43
|
### 🏗️ Architecture - Modular CLI Refactoring
|
package/README.md
CHANGED
|
@@ -31,9 +31,16 @@ git commit -m "auto"
|
|
|
31
31
|
|
|
32
32
|
```bash
|
|
33
33
|
claude-hooks create-pr develop
|
|
34
|
+
# Automatically pushes branch if unpublished (with confirmation)
|
|
34
35
|
# Analyzes diff, generates title/description, creates PR on GitHub
|
|
35
36
|
```
|
|
36
37
|
|
|
38
|
+
**Auto-push feature (v2.11.0):**
|
|
39
|
+
- Detects unpublished branches or unpushed commits
|
|
40
|
+
- Shows commit preview before pushing
|
|
41
|
+
- Prompts for confirmation (configurable)
|
|
42
|
+
- Handles diverged branches gracefully
|
|
43
|
+
|
|
37
44
|
### GitHub Token Setup
|
|
38
45
|
|
|
39
46
|
```bash
|
|
@@ -221,7 +228,16 @@ Preset always wins - tech-stack specific has priority over user preferences.
|
|
|
221
228
|
"version": "2.8.0",
|
|
222
229
|
"preset": "backend",
|
|
223
230
|
"overrides": {
|
|
224
|
-
"github": {
|
|
231
|
+
"github": {
|
|
232
|
+
"pr": {
|
|
233
|
+
"defaultBase": "develop",
|
|
234
|
+
"reviewers": ["user"],
|
|
235
|
+
"autoPush": true, // Auto-push unpublished branches (v2.11.0)
|
|
236
|
+
"pushConfirm": true, // Prompt before push (v2.11.0)
|
|
237
|
+
"showCommits": true, // Show commit preview (v2.11.0)
|
|
238
|
+
"verifyRemote": true // Verify remote exists (v2.11.0)
|
|
239
|
+
}
|
|
240
|
+
},
|
|
225
241
|
"subagents": { "batchSize": 2 }
|
|
226
242
|
}
|
|
227
243
|
}
|
|
@@ -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,101 @@ 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
|
+
|
|
139
235
|
// Step 6: Update remote and check for differences
|
|
140
236
|
logger.debug('create-pr', 'Step 6: Fetching latest changes from remote');
|
|
141
237
|
execSync('git fetch', { stdio: 'ignore' });
|
package/lib/config.js
CHANGED
|
@@ -101,6 +101,11 @@ const defaults = {
|
|
|
101
101
|
ai: ['ai', 'tooling'],
|
|
102
102
|
default: []
|
|
103
103
|
},
|
|
104
|
+
// Auto-push configuration (v2.11.0)
|
|
105
|
+
autoPush: true, // Auto-push unpublished branches
|
|
106
|
+
pushConfirm: true, // Prompt for confirmation before push
|
|
107
|
+
verifyRemote: true, // Verify remote exists before push
|
|
108
|
+
showCommits: true, // Show commit preview before push
|
|
104
109
|
},
|
|
105
110
|
},
|
|
106
111
|
|
|
@@ -328,6 +328,215 @@ const getStagedStats = () => {
|
|
|
328
328
|
}
|
|
329
329
|
};
|
|
330
330
|
|
|
331
|
+
/**
|
|
332
|
+
* Gets the configured remote name
|
|
333
|
+
* Why: Determines which remote to use for push operations (usually 'origin')
|
|
334
|
+
*
|
|
335
|
+
* @returns {string} Remote name (default: 'origin')
|
|
336
|
+
*/
|
|
337
|
+
const getRemoteName = () => {
|
|
338
|
+
logger.debug('git-operations - getRemoteName', 'Getting remote name');
|
|
339
|
+
|
|
340
|
+
try {
|
|
341
|
+
// Get list of remotes
|
|
342
|
+
const remotes = execGitCommand('remote');
|
|
343
|
+
const remoteList = remotes.split(/\r?\n/).filter(r => r.length > 0);
|
|
344
|
+
|
|
345
|
+
if (remoteList.length === 0) {
|
|
346
|
+
logger.debug('git-operations - getRemoteName', 'No remotes configured');
|
|
347
|
+
return 'origin'; // Default even if not configured
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// Prefer 'origin', otherwise use first remote
|
|
351
|
+
const remoteName = remoteList.includes('origin') ? 'origin' : remoteList[0];
|
|
352
|
+
|
|
353
|
+
logger.debug('git-operations - getRemoteName', 'Remote name determined', {
|
|
354
|
+
remoteName,
|
|
355
|
+
availableRemotes: remoteList
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
return remoteName;
|
|
359
|
+
|
|
360
|
+
} catch (error) {
|
|
361
|
+
logger.error('git-operations - getRemoteName', 'Failed to get remote name', error);
|
|
362
|
+
return 'origin'; // Fallback to default
|
|
363
|
+
}
|
|
364
|
+
};
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Verifies that a remote exists and is configured
|
|
368
|
+
* Why: Prevents push failures by checking remote configuration first
|
|
369
|
+
*
|
|
370
|
+
* @param {string} remoteName - Name of remote to verify (e.g., 'origin')
|
|
371
|
+
* @returns {boolean} True if remote exists, false otherwise
|
|
372
|
+
*/
|
|
373
|
+
const verifyRemoteExists = (remoteName) => {
|
|
374
|
+
logger.debug('git-operations - verifyRemoteExists', 'Verifying remote exists', { remoteName });
|
|
375
|
+
|
|
376
|
+
try {
|
|
377
|
+
const remotes = execGitCommand('remote');
|
|
378
|
+
const remoteList = remotes.split(/\r?\n/).filter(r => r.length > 0);
|
|
379
|
+
const exists = remoteList.includes(remoteName);
|
|
380
|
+
|
|
381
|
+
logger.debug('git-operations - verifyRemoteExists', 'Remote verification result', {
|
|
382
|
+
remoteName,
|
|
383
|
+
exists,
|
|
384
|
+
availableRemotes: remoteList
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
return exists;
|
|
388
|
+
|
|
389
|
+
} catch (error) {
|
|
390
|
+
logger.error('git-operations - verifyRemoteExists', 'Failed to verify remote', error);
|
|
391
|
+
return false;
|
|
392
|
+
}
|
|
393
|
+
};
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* Gets branch push status (remote tracking, unpushed commits, divergence)
|
|
397
|
+
* Why: Determines if and how to push before creating PR
|
|
398
|
+
*
|
|
399
|
+
* @param {string} branchName - Branch to check
|
|
400
|
+
* @returns {Object} Status object with:
|
|
401
|
+
* - hasRemote: boolean (branch exists on remote)
|
|
402
|
+
* - hasUnpushedCommits: boolean (local commits not pushed)
|
|
403
|
+
* - hasDiverged: boolean (local and remote histories differ)
|
|
404
|
+
* - upstreamBranch: string (e.g., 'origin/feature-branch')
|
|
405
|
+
* - unpushedCommits: Array<{sha, message}> (commits to push)
|
|
406
|
+
*/
|
|
407
|
+
const getBranchPushStatus = (branchName) => {
|
|
408
|
+
logger.debug('git-operations - getBranchPushStatus', 'Checking branch push status', { branchName });
|
|
409
|
+
|
|
410
|
+
const status = {
|
|
411
|
+
hasRemote: false,
|
|
412
|
+
hasUnpushedCommits: false,
|
|
413
|
+
hasDiverged: false,
|
|
414
|
+
upstreamBranch: null,
|
|
415
|
+
unpushedCommits: []
|
|
416
|
+
};
|
|
417
|
+
|
|
418
|
+
try {
|
|
419
|
+
// Check if branch has upstream tracking
|
|
420
|
+
// Use execSync directly to avoid logging expected "no upstream" errors
|
|
421
|
+
const upstream = execSync(
|
|
422
|
+
`git rev-parse --abbrev-ref --symbolic-full-name ${branchName}@{u}`,
|
|
423
|
+
{ encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }
|
|
424
|
+
).trim();
|
|
425
|
+
|
|
426
|
+
status.upstreamBranch = upstream;
|
|
427
|
+
status.hasRemote = true;
|
|
428
|
+
|
|
429
|
+
logger.debug('git-operations - getBranchPushStatus', 'Branch has upstream', {
|
|
430
|
+
branchName,
|
|
431
|
+
upstream
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
// Check for unpushed commits (local ahead of remote)
|
|
435
|
+
const unpushedLog = execGitCommand(`rev-list ${upstream}..${branchName} --oneline`);
|
|
436
|
+
if (unpushedLog) {
|
|
437
|
+
status.hasUnpushedCommits = true;
|
|
438
|
+
status.unpushedCommits = unpushedLog.split(/\r?\n/)
|
|
439
|
+
.filter(line => line.length > 0)
|
|
440
|
+
.map(line => {
|
|
441
|
+
const [sha, ...messageParts] = line.split(' ');
|
|
442
|
+
return {
|
|
443
|
+
sha,
|
|
444
|
+
message: messageParts.join(' ')
|
|
445
|
+
};
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// Check for divergence (remote has commits not in local)
|
|
450
|
+
const divergedLog = execGitCommand(`rev-list ${branchName}..${upstream} --oneline`);
|
|
451
|
+
if (divergedLog) {
|
|
452
|
+
status.hasDiverged = true;
|
|
453
|
+
logger.debug('git-operations - getBranchPushStatus', 'Branch has diverged', {
|
|
454
|
+
branchName,
|
|
455
|
+
remoteCommits: divergedLog.split(/\r?\n/).length
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
} catch (error) {
|
|
460
|
+
// No upstream means branch not published to remote (this is expected, not an error)
|
|
461
|
+
logger.debug('git-operations - getBranchPushStatus', 'Branch has no upstream (not published)', {
|
|
462
|
+
branchName
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
status.hasRemote = false;
|
|
466
|
+
status.hasUnpushedCommits = true; // All commits are unpushed
|
|
467
|
+
|
|
468
|
+
// Get all commits on this branch (not on any remote branch)
|
|
469
|
+
try {
|
|
470
|
+
const remoteName = getRemoteName();
|
|
471
|
+
const allCommits = execGitCommand(`rev-list ${branchName} --not --remotes=${remoteName} --oneline`);
|
|
472
|
+
if (allCommits) {
|
|
473
|
+
status.unpushedCommits = allCommits.split(/\r?\n/)
|
|
474
|
+
.filter(line => line.length > 0)
|
|
475
|
+
.map(line => {
|
|
476
|
+
const [sha, ...messageParts] = line.split(' ');
|
|
477
|
+
return {
|
|
478
|
+
sha,
|
|
479
|
+
message: messageParts.join(' ')
|
|
480
|
+
};
|
|
481
|
+
});
|
|
482
|
+
}
|
|
483
|
+
} catch (commitError) {
|
|
484
|
+
logger.debug('git-operations - getBranchPushStatus', 'Could not get unpushed commits', commitError);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
logger.debug('git-operations - getBranchPushStatus', 'Push status determined', status);
|
|
489
|
+
|
|
490
|
+
return status;
|
|
491
|
+
};
|
|
492
|
+
|
|
493
|
+
/**
|
|
494
|
+
* Pushes branch to remote
|
|
495
|
+
* Why: Publishes local branch to remote before creating PR
|
|
496
|
+
*
|
|
497
|
+
* @param {string} branchName - Branch to push
|
|
498
|
+
* @param {Object} options - Push options
|
|
499
|
+
* @param {boolean} options.setUpstream - Use -u flag to set upstream tracking (default: false)
|
|
500
|
+
* @returns {Object} Result object with:
|
|
501
|
+
* - success: boolean
|
|
502
|
+
* - output: string (stdout)
|
|
503
|
+
* - error: string (stderr)
|
|
504
|
+
*/
|
|
505
|
+
const pushBranch = (branchName, { setUpstream = false } = {}) => {
|
|
506
|
+
logger.debug('git-operations - pushBranch', 'Pushing branch', { branchName, setUpstream });
|
|
507
|
+
|
|
508
|
+
try {
|
|
509
|
+
const remoteName = getRemoteName();
|
|
510
|
+
const upstreamFlag = setUpstream ? '-u' : '';
|
|
511
|
+
const command = upstreamFlag
|
|
512
|
+
? `push ${upstreamFlag} ${remoteName} ${branchName}`
|
|
513
|
+
: `push ${remoteName} ${branchName}`;
|
|
514
|
+
|
|
515
|
+
const output = execGitCommand(command);
|
|
516
|
+
|
|
517
|
+
logger.debug('git-operations - pushBranch', 'Push successful', {
|
|
518
|
+
branchName,
|
|
519
|
+
remoteName,
|
|
520
|
+
setUpstream
|
|
521
|
+
});
|
|
522
|
+
|
|
523
|
+
return {
|
|
524
|
+
success: true,
|
|
525
|
+
output,
|
|
526
|
+
error: ''
|
|
527
|
+
};
|
|
528
|
+
|
|
529
|
+
} catch (error) {
|
|
530
|
+
logger.error('git-operations - pushBranch', 'Push failed', error);
|
|
531
|
+
|
|
532
|
+
return {
|
|
533
|
+
success: false,
|
|
534
|
+
output: error.output || '',
|
|
535
|
+
error: error.cause?.message || error.message || 'Unknown push error'
|
|
536
|
+
};
|
|
537
|
+
}
|
|
538
|
+
};
|
|
539
|
+
|
|
331
540
|
export {
|
|
332
541
|
GitError,
|
|
333
542
|
getStagedFiles,
|
|
@@ -337,5 +546,9 @@ export {
|
|
|
337
546
|
getRepoRoot,
|
|
338
547
|
getCurrentBranch,
|
|
339
548
|
getRepoName,
|
|
340
|
-
getStagedStats
|
|
549
|
+
getStagedStats,
|
|
550
|
+
getRemoteName,
|
|
551
|
+
verifyRemoteExists,
|
|
552
|
+
getBranchPushStatus,
|
|
553
|
+
pushBranch
|
|
341
554
|
};
|