git-watchtower 1.9.1 → 1.9.3

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.
@@ -82,6 +82,7 @@ const { parseGitHubPr, parseGitLabMr, parseGitHubPrList, parseGitLabMrList, isBa
82
82
  // Security & Validation (imported from src/git/branch.js and src/git/commands.js)
83
83
  // ============================================================================
84
84
  const { isValidBranchName, sanitizeBranchName, getGoneBranches, deleteGoneBranches } = require('../src/git/branch');
85
+ const { pruneStaleEntries } = require('../src/polling/engine');
85
86
  const { isGitAvailable: checkGitAvailable, execGit, execGitSilent, getDiffStats: getDiffStatsSafe, getAheadBehind, getDiffShortstat } = require('../src/git/commands');
86
87
 
87
88
  // Session stats (always-on, non-casino stats)
@@ -993,11 +994,11 @@ async function refreshAllSparklines() {
993
994
  return; // Don't refresh too often
994
995
  }
995
996
 
996
- try {
997
- const currentBranches = store.get('branches');
998
- for (const branch of currentBranches.slice(0, 20)) { // Limit to top 20
999
- if (branch.isDeleted) continue;
997
+ const currentBranches = store.get('branches');
998
+ for (const branch of currentBranches.slice(0, 20)) { // Limit to top 20
999
+ if (branch.isDeleted) continue;
1000
1000
 
1001
+ try {
1001
1002
  // Get commit counts for last 7 days (try remote, fall back to local)
1002
1003
  const sparkResult = await execGitSilent(
1003
1004
  ['log', `origin/${branch.name}`, '--since=7 days ago', '--format=%ad', '--date=format:%Y-%m-%d'],
@@ -1027,11 +1028,11 @@ async function refreshAllSparklines() {
1027
1028
 
1028
1029
  const counts = Array.from(dayCounts.values());
1029
1030
  store.get('sparklineCache').set(branch.name, generateSparkline(counts));
1031
+ } catch (e) {
1032
+ // Skip this branch - don't let one failure abort all sparkline updates
1030
1033
  }
1031
- lastSparklineUpdate = now;
1032
- } catch (e) {
1033
- // Silently fail - sparklines are optional
1034
1034
  }
1035
+ lastSparklineUpdate = now;
1035
1036
  }
1036
1037
 
1037
1038
  async function getPreviewData(branchName) {
@@ -1817,6 +1818,16 @@ async function pollGitChanges() {
1817
1818
  }
1818
1819
  }
1819
1820
 
1821
+ // Prune stale entries: remove branches from tracking sets/caches
1822
+ // that no longer exist in git (deleted >30s ago or already gone)
1823
+ pruneStaleEntries({
1824
+ knownBranchNames,
1825
+ fetchedBranchNames,
1826
+ allBranches,
1827
+ caches: [previousBranchStates, prInfoCache, store.get('sparklineCache'), store.get('aheadBehindCache')],
1828
+ now,
1829
+ });
1830
+
1820
1831
  // Note: isNew flag is only cleared when branch becomes current (see below)
1821
1832
 
1822
1833
  // Keep deleted branches in the list (don't remove them)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "git-watchtower",
3
- "version": "1.9.1",
3
+ "version": "1.9.3",
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": {
@@ -147,6 +147,38 @@ function restoreSelection(branches, previousName, previousIndex) {
147
147
  return { selectedIndex: previousIndex, selectedBranchName: previousName };
148
148
  }
149
149
 
150
+ /**
151
+ * Prune stale entries from tracking sets and caches for branches
152
+ * that no longer exist in git.
153
+ * @param {Object} opts
154
+ * @param {Set<string>} opts.knownBranchNames - Known branch names (mutated)
155
+ * @param {Set<string>} opts.fetchedBranchNames - Currently fetched branch names
156
+ * @param {Array<{name: string, isDeleted?: boolean, deletedAt?: number}>} opts.allBranches - All branches including deleted
157
+ * @param {Map<string, *>[]} opts.caches - Maps to prune stale keys from
158
+ * @param {number} [opts.retentionMs=30000] - How long to keep deleted branches before pruning
159
+ * @param {number} [opts.now] - Current timestamp (defaults to Date.now())
160
+ * @returns {string[]} Names of pruned branches
161
+ */
162
+ function pruneStaleEntries({ knownBranchNames, fetchedBranchNames, allBranches, caches, retentionMs = 30000, now }) {
163
+ const timestamp = now ?? Date.now();
164
+ const pruned = [];
165
+ for (const knownName of [...knownBranchNames]) {
166
+ if (fetchedBranchNames.has(knownName)) continue;
167
+ const deletedBranch = allBranches.find(b => b.name === knownName && b.isDeleted);
168
+ const shouldPrune = deletedBranch
169
+ ? (deletedBranch.deletedAt && (timestamp - deletedBranch.deletedAt) > retentionMs)
170
+ : true; // Not in allBranches at all — stale entry
171
+ if (shouldPrune) {
172
+ knownBranchNames.delete(knownName);
173
+ for (const cache of caches) {
174
+ cache.delete(knownName);
175
+ }
176
+ pruned.push(knownName);
177
+ }
178
+ }
179
+ return pruned;
180
+ }
181
+
150
182
  module.exports = {
151
183
  detectNewBranches,
152
184
  detectDeletedBranches,
@@ -154,4 +186,5 @@ module.exports = {
154
186
  sortBranches,
155
187
  calculateAdaptiveInterval,
156
188
  restoreSelection,
189
+ pruneStaleEntries,
157
190
  };