git-watchtower 1.4.0 → 1.6.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/bin/git-watchtower.js +160 -23
- package/package.json +1 -1
package/bin/git-watchtower.js
CHANGED
|
@@ -77,7 +77,7 @@ const { parseGitHubPr, parseGitLabMr, parseGitHubPrList, parseGitLabMrList, isBa
|
|
|
77
77
|
// ============================================================================
|
|
78
78
|
// Security & Validation (imported from src/git/branch.js and src/git/commands.js)
|
|
79
79
|
// ============================================================================
|
|
80
|
-
const { isValidBranchName, sanitizeBranchName } = require('../src/git/branch');
|
|
80
|
+
const { isValidBranchName, sanitizeBranchName, getGoneBranches, deleteGoneBranches } = require('../src/git/branch');
|
|
81
81
|
const { isGitAvailable: checkGitAvailable } = require('../src/git/commands');
|
|
82
82
|
|
|
83
83
|
// ============================================================================
|
|
@@ -1190,6 +1190,16 @@ function render() {
|
|
|
1190
1190
|
if (state.errorToast) {
|
|
1191
1191
|
renderer.renderErrorToast(state, write);
|
|
1192
1192
|
}
|
|
1193
|
+
|
|
1194
|
+
// Cleanup confirmation dialog
|
|
1195
|
+
if (state.cleanupConfirmMode) {
|
|
1196
|
+
renderer.renderCleanupConfirm(state, write);
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
// Stash confirmation dialog renders on top of everything
|
|
1200
|
+
if (state.stashConfirmMode) {
|
|
1201
|
+
renderer.renderStashConfirm(state, write);
|
|
1202
|
+
}
|
|
1193
1203
|
}
|
|
1194
1204
|
|
|
1195
1205
|
function showFlash(message) {
|
|
@@ -1224,7 +1234,6 @@ function showErrorToast(title, message, hint = null, duration = 8000) {
|
|
|
1224
1234
|
|
|
1225
1235
|
errorToastTimeout = setTimeout(() => {
|
|
1226
1236
|
store.setState({ errorToast: null });
|
|
1227
|
-
pendingDirtyOperation = null;
|
|
1228
1237
|
render();
|
|
1229
1238
|
}, duration);
|
|
1230
1239
|
}
|
|
@@ -1240,6 +1249,26 @@ function hideErrorToast() {
|
|
|
1240
1249
|
}
|
|
1241
1250
|
}
|
|
1242
1251
|
|
|
1252
|
+
function showStashConfirm(operationLabel) {
|
|
1253
|
+
store.setState({
|
|
1254
|
+
stashConfirmMode: true,
|
|
1255
|
+
stashConfirmSelectedIndex: 0,
|
|
1256
|
+
pendingDirtyOperationLabel: operationLabel,
|
|
1257
|
+
});
|
|
1258
|
+
render();
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
function hideStashConfirm() {
|
|
1262
|
+
if (store.get('stashConfirmMode')) {
|
|
1263
|
+
store.setState({
|
|
1264
|
+
stashConfirmMode: false,
|
|
1265
|
+
stashConfirmSelectedIndex: 0,
|
|
1266
|
+
pendingDirtyOperationLabel: null,
|
|
1267
|
+
});
|
|
1268
|
+
render();
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1243
1272
|
// ============================================================================
|
|
1244
1273
|
// Git Functions
|
|
1245
1274
|
// ============================================================================
|
|
@@ -1365,13 +1394,8 @@ async function switchToBranch(branchName, recordHistory = true) {
|
|
|
1365
1394
|
const isDirty = await hasUncommittedChanges();
|
|
1366
1395
|
if (isDirty) {
|
|
1367
1396
|
addLog(`Cannot switch: uncommitted changes in working directory`, 'error');
|
|
1368
|
-
addLog(`Press S to stash changes, or commit manually`, 'warning');
|
|
1369
1397
|
pendingDirtyOperation = { type: 'switch', branch: branchName };
|
|
1370
|
-
|
|
1371
|
-
'Cannot Switch Branch',
|
|
1372
|
-
'You have uncommitted changes in your working directory that would be lost.',
|
|
1373
|
-
'Press S to stash changes'
|
|
1374
|
-
);
|
|
1398
|
+
showStashConfirm(`switch to ${branchName}`);
|
|
1375
1399
|
return { success: false, reason: 'dirty' };
|
|
1376
1400
|
}
|
|
1377
1401
|
|
|
@@ -1404,6 +1428,7 @@ async function switchToBranch(branchName, recordHistory = true) {
|
|
|
1404
1428
|
}
|
|
1405
1429
|
|
|
1406
1430
|
addLog(`Switched to ${safeBranchName}`, 'success');
|
|
1431
|
+
pendingDirtyOperation = null;
|
|
1407
1432
|
|
|
1408
1433
|
// Restart server if configured (command mode)
|
|
1409
1434
|
if (SERVER_MODE === 'command' && RESTART_ON_SWITCH && serverProcess) {
|
|
@@ -1423,13 +1448,8 @@ async function switchToBranch(branchName, recordHistory = true) {
|
|
|
1423
1448
|
);
|
|
1424
1449
|
} else if (errMsg.includes('local changes') || errMsg.includes('overwritten')) {
|
|
1425
1450
|
addLog(`Cannot switch: local changes would be overwritten`, 'error');
|
|
1426
|
-
addLog(`Press S to stash changes, or commit manually`, 'warning');
|
|
1427
1451
|
pendingDirtyOperation = { type: 'switch', branch: branchName };
|
|
1428
|
-
|
|
1429
|
-
'Cannot Switch Branch',
|
|
1430
|
-
'Your local changes would be overwritten by checkout.',
|
|
1431
|
-
'Press S to stash changes'
|
|
1432
|
-
);
|
|
1452
|
+
showStashConfirm(`switch to ${branchName}`);
|
|
1433
1453
|
} else {
|
|
1434
1454
|
addLog(`Failed to switch: ${errMsg}`, 'error');
|
|
1435
1455
|
showErrorToast(
|
|
@@ -1481,6 +1501,7 @@ async function pullCurrentBranch() {
|
|
|
1481
1501
|
|
|
1482
1502
|
await execAsync(`git pull "${REMOTE_NAME}" "${branch}"`);
|
|
1483
1503
|
addLog('Pulled successfully', 'success');
|
|
1504
|
+
pendingDirtyOperation = null;
|
|
1484
1505
|
notifyClients();
|
|
1485
1506
|
return { success: true };
|
|
1486
1507
|
} catch (e) {
|
|
@@ -1488,13 +1509,8 @@ async function pullCurrentBranch() {
|
|
|
1488
1509
|
addLog(`Pull failed: ${errMsg}`, 'error');
|
|
1489
1510
|
|
|
1490
1511
|
if (errMsg.includes('local changes') || errMsg.includes('overwritten') || errMsg.includes('uncommitted changes')) {
|
|
1491
|
-
addLog(`Press S to stash changes, or commit manually`, 'warning');
|
|
1492
1512
|
pendingDirtyOperation = { type: 'pull' };
|
|
1493
|
-
|
|
1494
|
-
'Pull Failed',
|
|
1495
|
-
'Your local changes would be overwritten by pull.',
|
|
1496
|
-
'Press S to stash changes'
|
|
1497
|
-
);
|
|
1513
|
+
showStashConfirm('pull');
|
|
1498
1514
|
} else if (isMergeConflict(errMsg)) {
|
|
1499
1515
|
store.setState({ hasMergeConflict: true });
|
|
1500
1516
|
showErrorToast(
|
|
@@ -1535,6 +1551,7 @@ async function stashAndRetry() {
|
|
|
1535
1551
|
|
|
1536
1552
|
pendingDirtyOperation = null;
|
|
1537
1553
|
hideErrorToast();
|
|
1554
|
+
hideStashConfirm();
|
|
1538
1555
|
|
|
1539
1556
|
addLog('Stashing uncommitted changes...', 'update');
|
|
1540
1557
|
render();
|
|
@@ -1556,9 +1573,13 @@ async function stashAndRetry() {
|
|
|
1556
1573
|
const popResult = await gitStashPop();
|
|
1557
1574
|
if (popResult.success) {
|
|
1558
1575
|
addLog('Stashed changes restored', 'info');
|
|
1576
|
+
showFlash('Stashed changes restored (switch failed)');
|
|
1559
1577
|
} else {
|
|
1560
1578
|
addLog('Warning: could not restore stashed changes. Run: git stash pop', 'error');
|
|
1579
|
+
showErrorToast('Stash Pop Failed', 'Could not restore stashed changes.', 'Run: git stash pop');
|
|
1561
1580
|
}
|
|
1581
|
+
} else {
|
|
1582
|
+
showFlash(`Stashed & switched to ${operation.branch}`);
|
|
1562
1583
|
}
|
|
1563
1584
|
await pollGitChanges();
|
|
1564
1585
|
} else if (operation.type === 'pull') {
|
|
@@ -1568,9 +1589,13 @@ async function stashAndRetry() {
|
|
|
1568
1589
|
const popResult = await gitStashPop();
|
|
1569
1590
|
if (popResult.success) {
|
|
1570
1591
|
addLog('Stashed changes restored', 'info');
|
|
1592
|
+
showFlash('Stashed changes restored (pull failed)');
|
|
1571
1593
|
} else {
|
|
1572
1594
|
addLog('Warning: could not restore stashed changes. Run: git stash pop', 'error');
|
|
1595
|
+
showErrorToast('Stash Pop Failed', 'Could not restore stashed changes.', 'Run: git stash pop');
|
|
1573
1596
|
}
|
|
1597
|
+
} else {
|
|
1598
|
+
showFlash('Stashed & pulled successfully');
|
|
1574
1599
|
}
|
|
1575
1600
|
await pollGitChanges();
|
|
1576
1601
|
}
|
|
@@ -2309,6 +2334,105 @@ function setupKeyboardInput() {
|
|
|
2309
2334
|
return; // Ignore other keys in action mode
|
|
2310
2335
|
}
|
|
2311
2336
|
|
|
2337
|
+
// Handle cleanup confirmation dialog
|
|
2338
|
+
if (store.get('cleanupConfirmMode')) {
|
|
2339
|
+
const cleanupBranches = store.get('cleanupBranches') || [];
|
|
2340
|
+
const maxOptions = cleanupBranches.length > 0 ? 3 : 1;
|
|
2341
|
+
if (key === '\u001b[A' || key === 'k') { // Up
|
|
2342
|
+
const idx = store.get('cleanupSelectedIndex') || 0;
|
|
2343
|
+
if (idx > 0) {
|
|
2344
|
+
store.setState({ cleanupSelectedIndex: idx - 1 });
|
|
2345
|
+
render();
|
|
2346
|
+
}
|
|
2347
|
+
return;
|
|
2348
|
+
}
|
|
2349
|
+
if (key === '\u001b[B' || key === 'j') { // Down
|
|
2350
|
+
const idx = store.get('cleanupSelectedIndex') || 0;
|
|
2351
|
+
if (idx < maxOptions - 1) {
|
|
2352
|
+
store.setState({ cleanupSelectedIndex: idx + 1 });
|
|
2353
|
+
render();
|
|
2354
|
+
}
|
|
2355
|
+
return;
|
|
2356
|
+
}
|
|
2357
|
+
if (key === '\r' || key === '\n') { // Enter — execute selected option
|
|
2358
|
+
const idx = store.get('cleanupSelectedIndex') || 0;
|
|
2359
|
+
applyUpdates(actions.closeCleanupConfirm(getActionState()));
|
|
2360
|
+
render();
|
|
2361
|
+
if (cleanupBranches.length === 0 || idx === maxOptions - 1) {
|
|
2362
|
+
// Cancel or Close (no branches)
|
|
2363
|
+
return;
|
|
2364
|
+
}
|
|
2365
|
+
const force = idx === 1; // 0=safe delete, 1=force delete, 2=cancel
|
|
2366
|
+
addLog(`Cleaning up ${cleanupBranches.length} stale branch${cleanupBranches.length === 1 ? '' : 'es'}${force ? ' (force)' : ''}...`, 'update');
|
|
2367
|
+
render();
|
|
2368
|
+
const result = await deleteGoneBranches(cleanupBranches, { force });
|
|
2369
|
+
for (const name of result.deleted) {
|
|
2370
|
+
addLog(`Deleted branch: ${name}`, 'success');
|
|
2371
|
+
}
|
|
2372
|
+
for (const f of result.failed) {
|
|
2373
|
+
addLog(`Failed to delete ${f.name}: ${f.error}`, 'error');
|
|
2374
|
+
}
|
|
2375
|
+
if (result.deleted.length > 0) {
|
|
2376
|
+
addLog(`Cleaned up ${result.deleted.length} branch${result.deleted.length === 1 ? '' : 'es'}`, 'success');
|
|
2377
|
+
await pollGitChanges();
|
|
2378
|
+
}
|
|
2379
|
+
render();
|
|
2380
|
+
return;
|
|
2381
|
+
}
|
|
2382
|
+
if (key === '\u001b') { // Escape — cancel
|
|
2383
|
+
applyUpdates(actions.closeCleanupConfirm(getActionState()));
|
|
2384
|
+
render();
|
|
2385
|
+
return;
|
|
2386
|
+
}
|
|
2387
|
+
return; // Ignore other keys in cleanup mode
|
|
2388
|
+
}
|
|
2389
|
+
|
|
2390
|
+
// Handle stash confirmation dialog
|
|
2391
|
+
if (store.get('stashConfirmMode')) {
|
|
2392
|
+
if (key === '\u001b[A' || key === 'k') { // Up
|
|
2393
|
+
const idx = store.get('stashConfirmSelectedIndex');
|
|
2394
|
+
if (idx > 0) {
|
|
2395
|
+
store.setState({ stashConfirmSelectedIndex: idx - 1 });
|
|
2396
|
+
render();
|
|
2397
|
+
}
|
|
2398
|
+
return;
|
|
2399
|
+
}
|
|
2400
|
+
if (key === '\u001b[B' || key === 'j') { // Down
|
|
2401
|
+
const idx = store.get('stashConfirmSelectedIndex');
|
|
2402
|
+
if (idx < 1) {
|
|
2403
|
+
store.setState({ stashConfirmSelectedIndex: idx + 1 });
|
|
2404
|
+
render();
|
|
2405
|
+
}
|
|
2406
|
+
return;
|
|
2407
|
+
}
|
|
2408
|
+
if (key === '\r' || key === '\n') { // Enter — execute selected option
|
|
2409
|
+
const idx = store.get('stashConfirmSelectedIndex');
|
|
2410
|
+
hideStashConfirm();
|
|
2411
|
+
if (idx === 0 && pendingDirtyOperation) {
|
|
2412
|
+
await stashAndRetry();
|
|
2413
|
+
} else {
|
|
2414
|
+
addLog('Stash cancelled — handle changes manually', 'info');
|
|
2415
|
+
pendingDirtyOperation = null;
|
|
2416
|
+
}
|
|
2417
|
+
return;
|
|
2418
|
+
}
|
|
2419
|
+
if (key === 'S') { // S shortcut — stash directly
|
|
2420
|
+
hideStashConfirm();
|
|
2421
|
+
if (pendingDirtyOperation) {
|
|
2422
|
+
await stashAndRetry();
|
|
2423
|
+
}
|
|
2424
|
+
return;
|
|
2425
|
+
}
|
|
2426
|
+
if (key === '\u001b') { // Escape — cancel
|
|
2427
|
+
hideStashConfirm();
|
|
2428
|
+
addLog('Stash cancelled — handle changes manually', 'info');
|
|
2429
|
+
pendingDirtyOperation = null;
|
|
2430
|
+
render();
|
|
2431
|
+
return;
|
|
2432
|
+
}
|
|
2433
|
+
return; // Ignore other keys in stash confirm mode
|
|
2434
|
+
}
|
|
2435
|
+
|
|
2312
2436
|
// Dismiss flash on any key
|
|
2313
2437
|
if (store.get('flashMessage')) {
|
|
2314
2438
|
hideFlash();
|
|
@@ -2324,7 +2448,6 @@ function setupKeyboardInput() {
|
|
|
2324
2448
|
return;
|
|
2325
2449
|
}
|
|
2326
2450
|
hideErrorToast();
|
|
2327
|
-
pendingDirtyOperation = null;
|
|
2328
2451
|
if (key !== '\u001b[A' && key !== '\u001b[B' && key !== '\r' && key !== 'q') {
|
|
2329
2452
|
return;
|
|
2330
2453
|
}
|
|
@@ -2475,9 +2598,14 @@ function setupKeyboardInput() {
|
|
|
2475
2598
|
break;
|
|
2476
2599
|
}
|
|
2477
2600
|
|
|
2478
|
-
case 'S': // Stash changes
|
|
2601
|
+
case 'S': // Stash changes — open confirm dialog or show hint
|
|
2479
2602
|
if (pendingDirtyOperation) {
|
|
2480
|
-
|
|
2603
|
+
const label = pendingDirtyOperation.type === 'switch'
|
|
2604
|
+
? `switch to ${pendingDirtyOperation.branch}`
|
|
2605
|
+
: 'pull';
|
|
2606
|
+
showStashConfirm(label);
|
|
2607
|
+
} else {
|
|
2608
|
+
showFlash('No pending operation — stash with S after a failed switch or pull');
|
|
2481
2609
|
}
|
|
2482
2610
|
break;
|
|
2483
2611
|
|
|
@@ -2495,6 +2623,15 @@ function setupKeyboardInput() {
|
|
|
2495
2623
|
break;
|
|
2496
2624
|
}
|
|
2497
2625
|
|
|
2626
|
+
case 'd': { // Cleanup stale branches (remotes deleted)
|
|
2627
|
+
addLog('Scanning for stale branches...', 'info');
|
|
2628
|
+
render();
|
|
2629
|
+
const goneBranches = await getGoneBranches();
|
|
2630
|
+
applyUpdates(actions.openCleanupConfirm(actionState, goneBranches));
|
|
2631
|
+
render();
|
|
2632
|
+
break;
|
|
2633
|
+
}
|
|
2634
|
+
|
|
2498
2635
|
// Number keys to set visible branch count
|
|
2499
2636
|
case '1': case '2': case '3': case '4': case '5':
|
|
2500
2637
|
case '6': case '7': case '8': case '9':
|
package/package.json
CHANGED