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.
Files changed (2) hide show
  1. package/bin/git-watchtower.js +160 -23
  2. package/package.json +1 -1
@@ -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
- showErrorToast(
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
- showErrorToast(
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
- showErrorToast(
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 (only active with pending dirty operation)
2601
+ case 'S': // Stash changes open confirm dialog or show hint
2479
2602
  if (pendingDirtyOperation) {
2480
- await stashAndRetry();
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "git-watchtower",
3
- "version": "1.4.0",
3
+ "version": "1.6.0",
4
4
  "description": "Terminal-based Git branch monitor with activity sparklines and optional dev server with live reload",
5
5
  "main": "bin/git-watchtower.js",
6
6
  "bin": {