git-watchtower 1.9.4 → 1.9.5

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.
@@ -81,9 +81,9 @@ const { parseGitHubPr, parseGitLabMr, parseGitHubPrList, parseGitLabMrList, isBa
81
81
  // ============================================================================
82
82
  // Security & Validation (imported from src/git/branch.js and src/git/commands.js)
83
83
  // ============================================================================
84
- const { isValidBranchName, sanitizeBranchName, getGoneBranches, deleteGoneBranches } = require('../src/git/branch');
84
+ const { isValidBranchName, sanitizeBranchName, getGoneBranches, deleteGoneBranches, getCurrentBranch: getCurrentBranchRaw, getAllBranches: getAllBranchesRaw } = require('../src/git/branch');
85
85
  const { pruneStaleEntries } = require('../src/polling/engine');
86
- const { isGitAvailable: checkGitAvailable, execGit, execGitSilent, getDiffStats: getDiffStatsSafe, getAheadBehind, getDiffShortstat } = require('../src/git/commands');
86
+ const { isGitAvailable: checkGitAvailable, execGit, execGitSilent, getDiffStats: getDiffStatsSafe, getAheadBehind, getDiffShortstat, hasUncommittedChanges: checkUncommittedChanges } = require('../src/git/commands');
87
87
 
88
88
  // Session stats (always-on, non-casino stats)
89
89
  const sessionStats = require('../src/stats/session');
@@ -743,8 +743,9 @@ const actions = require('../src/ui/actions');
743
743
  // Diff stats parsing and stash imported from src/git/commands.js
744
744
  const { parseDiffStats, stash: gitStash, stashPop: gitStashPop } = require('../src/git/commands');
745
745
 
746
- // Server process command parsing
746
+ // Server process command parsing and static server utilities
747
747
  const { parseCommand } = require('../src/server/process');
748
+ const { getMimeType, injectLiveReload } = require('../src/server/static');
748
749
 
749
750
  // State (non-store globals)
750
751
  let previousBranchStates = new Map(); // branch name -> commit hash
@@ -781,39 +782,7 @@ const MAX_HISTORY = 20;
781
782
  let lastSparklineUpdate = 0;
782
783
  const SPARKLINE_CACHE_TTL = 5 * 60 * 1000; // 5 minutes
783
784
 
784
- // MIME types
785
- const MIME_TYPES = {
786
- '.html': 'text/html',
787
- '.css': 'text/css',
788
- '.js': 'application/javascript',
789
- '.json': 'application/json',
790
- '.png': 'image/png',
791
- '.jpg': 'image/jpeg',
792
- '.jpeg': 'image/jpeg',
793
- '.gif': 'image/gif',
794
- '.svg': 'image/svg+xml',
795
- '.ico': 'image/x-icon',
796
- '.webp': 'image/webp',
797
- '.woff': 'font/woff',
798
- '.woff2': 'font/woff2',
799
- '.ttf': 'font/ttf',
800
- '.xml': 'application/xml',
801
- '.txt': 'text/plain',
802
- '.md': 'text/markdown',
803
- '.pdf': 'application/pdf',
804
- };
805
-
806
- // Live reload script
807
- const LIVE_RELOAD_SCRIPT = `
808
- <script>
809
- (function() {
810
- var source = new EventSource('/livereload');
811
- source.onmessage = function(e) {
812
- if (e.data === 'reload') location.reload();
813
- };
814
- })();
815
- </script>
816
- </body>`;
785
+ // MIME_TYPES and LIVE_RELOAD_SCRIPT imported from src/server/static.js (via getMimeType and injectLiveReload)
817
786
 
818
787
  // ============================================================================
819
788
  // Utility Functions
@@ -1379,20 +1348,9 @@ function hideStashConfirm() {
1379
1348
  // ============================================================================
1380
1349
 
1381
1350
  async function getCurrentBranch() {
1382
- try {
1383
- const { stdout } = await execGit(['rev-parse', '--abbrev-ref', 'HEAD'], { cwd: PROJECT_ROOT });
1384
- // Check for detached HEAD state
1385
- if (stdout === 'HEAD') {
1386
- store.setState({ isDetachedHead: true });
1387
- // Get the short commit hash instead
1388
- const { stdout: commitHash } = await execGit(['rev-parse', '--short', 'HEAD'], { cwd: PROJECT_ROOT });
1389
- return `HEAD@${commitHash}`;
1390
- }
1391
- store.setState({ isDetachedHead: false });
1392
- return stdout;
1393
- } catch (e) {
1394
- return null;
1395
- }
1351
+ const result = await getCurrentBranchRaw(PROJECT_ROOT);
1352
+ store.setState({ isDetachedHead: result.isDetached });
1353
+ return result.name;
1396
1354
  }
1397
1355
 
1398
1356
  async function checkRemoteExists() {
@@ -1406,91 +1364,14 @@ async function checkRemoteExists() {
1406
1364
  }
1407
1365
 
1408
1366
  async function hasUncommittedChanges() {
1409
- try {
1410
- const { stdout } = await execGit(['status', '--porcelain'], { cwd: PROJECT_ROOT, timeout: 5000 });
1411
- return stdout.length > 0;
1412
- } catch (e) {
1413
- return false;
1414
- }
1367
+ return checkUncommittedChanges(PROJECT_ROOT);
1415
1368
  }
1416
1369
 
1417
1370
  // isAuthError, isMergeConflict, isNetworkError imported from src/utils/errors.js
1418
1371
 
1419
1372
  async function getAllBranches() {
1420
1373
  try {
1421
- await execGitSilent(['fetch', '--all', '--prune'], { cwd: PROJECT_ROOT, timeout: 60000 });
1422
-
1423
- const branchList = [];
1424
- const seenBranches = new Set();
1425
-
1426
- // Get local branches
1427
- // Use \x1f (Unit Separator) as delimiter since | can appear in commit subjects
1428
- const delimiter = '\x1f';
1429
- const { stdout: localOutput } = await execGit(
1430
- ['for-each-ref', '--sort=-committerdate', `--format=%(refname:short)${delimiter}%(committerdate:iso8601)${delimiter}%(objectname:short)${delimiter}%(subject)`, 'refs/heads/'],
1431
- { cwd: PROJECT_ROOT }
1432
- );
1433
-
1434
- for (const line of localOutput.split('\n').filter(Boolean)) {
1435
- const [name, dateStr, commit, ...subjectParts] = line.split(delimiter);
1436
- const subject = subjectParts.join(delimiter);
1437
- if (!seenBranches.has(name) && isValidBranchName(name)) {
1438
- seenBranches.add(name);
1439
- branchList.push({
1440
- name,
1441
- commit,
1442
- subject: subject || '',
1443
- date: new Date(dateStr),
1444
- isLocal: true,
1445
- hasRemote: false,
1446
- hasUpdates: false,
1447
- });
1448
- }
1449
- }
1450
-
1451
- // Get remote branches (using configured remote name)
1452
- const remoteResult = await execGitSilent(
1453
- ['for-each-ref', '--sort=-committerdate', `--format=%(refname:short)${delimiter}%(committerdate:iso8601)${delimiter}%(objectname:short)${delimiter}%(subject)`, `refs/remotes/${REMOTE_NAME}/`],
1454
- { cwd: PROJECT_ROOT }
1455
- );
1456
- const remoteOutput = remoteResult ? remoteResult.stdout : '';
1457
-
1458
- const remotePrefix = `${REMOTE_NAME}/`;
1459
- for (const line of remoteOutput.split('\n').filter(Boolean)) {
1460
- const [fullName, dateStr, commit, ...subjectParts] = line.split(delimiter);
1461
- const subject = subjectParts.join(delimiter);
1462
- const name = fullName.replace(remotePrefix, '');
1463
- if (name === 'HEAD') continue;
1464
- if (!isValidBranchName(name)) continue;
1465
-
1466
- const existing = branchList.find(b => b.name === name);
1467
- if (existing) {
1468
- existing.hasRemote = true;
1469
- existing.remoteCommit = commit;
1470
- existing.remoteDate = new Date(dateStr);
1471
- existing.remoteSubject = subject || '';
1472
- if (commit !== existing.commit) {
1473
- existing.hasUpdates = true;
1474
- // Use remote's date when it has updates (so it sorts to top)
1475
- existing.date = new Date(dateStr);
1476
- existing.subject = subject || existing.subject;
1477
- }
1478
- } else if (!seenBranches.has(name)) {
1479
- seenBranches.add(name);
1480
- branchList.push({
1481
- name,
1482
- commit,
1483
- subject: subject || '',
1484
- date: new Date(dateStr),
1485
- isLocal: false,
1486
- hasRemote: true,
1487
- hasUpdates: false,
1488
- });
1489
- }
1490
- }
1491
-
1492
- branchList.sort((a, b) => b.date - a.date);
1493
- return branchList; // Return all branches, caller will slice
1374
+ return await getAllBranchesRaw({ remoteName: REMOTE_NAME, fetch: true, cwd: PROJECT_ROOT });
1494
1375
  } catch (e) {
1495
1376
  addLog(`Failed to get branches: ${e.message || e}`, 'error');
1496
1377
  return [];
@@ -1841,6 +1722,8 @@ async function pollGitChanges() {
1841
1722
  const updatedBranches = [];
1842
1723
  const currentBranchName = store.get('currentBranch');
1843
1724
  for (const branch of pollFilteredBranches) {
1725
+ // Clear previous cycle's flag so only freshly-updated branches are highlighted
1726
+ branch.justUpdated = false;
1844
1727
  if (branch.isDeleted) continue;
1845
1728
  const prevCommit = previousBranchStates.get(branch.name);
1846
1729
  if (prevCommit && prevCommit !== branch.commit && branch.name !== currentBranchName) {
@@ -2109,7 +1992,7 @@ function handleLiveReload(req, res) {
2109
1992
 
2110
1993
  function serveFile(res, filePath, logPath) {
2111
1994
  const ext = path.extname(filePath).toLowerCase();
2112
- const mimeType = MIME_TYPES[ext] || 'application/octet-stream';
1995
+ const mimeType = getMimeType(ext);
2113
1996
 
2114
1997
  fs.readFile(filePath, (err, data) => {
2115
1998
  if (err) {
@@ -2120,10 +2003,7 @@ function serveFile(res, filePath, logPath) {
2120
2003
  }
2121
2004
 
2122
2005
  if (mimeType === 'text/html') {
2123
- let html = data.toString();
2124
- if (html.includes('</body>')) {
2125
- html = html.replace('</body>', LIVE_RELOAD_SCRIPT);
2126
- }
2006
+ const html = injectLiveReload(data.toString());
2127
2007
  res.writeHead(200, { 'Content-Type': mimeType });
2128
2008
  res.end(html);
2129
2009
  } else {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "git-watchtower",
3
- "version": "1.9.4",
3
+ "version": "1.9.5",
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": {
@@ -57,6 +57,8 @@ function detectDeletedBranches(knownBranchNames, fetchedBranchNames, existingBra
57
57
  function detectUpdatedBranches(branches, previousStates, currentBranch) {
58
58
  const updated = [];
59
59
  for (const branch of branches) {
60
+ // Clear previous cycle's flag so only freshly-updated branches are highlighted
61
+ branch.justUpdated = false;
60
62
  if (branch.isDeleted) continue;
61
63
  const prevCommit = previousStates.get(branch.name);
62
64
  if (prevCommit && prevCommit !== branch.commit && branch.name !== currentBranch) {