git-watchtower 1.9.2 → 1.9.4

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)
@@ -1423,13 +1424,16 @@ async function getAllBranches() {
1423
1424
  const seenBranches = new Set();
1424
1425
 
1425
1426
  // Get local branches
1427
+ // Use \x1f (Unit Separator) as delimiter since | can appear in commit subjects
1428
+ const delimiter = '\x1f';
1426
1429
  const { stdout: localOutput } = await execGit(
1427
- ['for-each-ref', '--sort=-committerdate', '--format=%(refname:short)|%(committerdate:iso8601)|%(objectname:short)|%(subject)', 'refs/heads/'],
1430
+ ['for-each-ref', '--sort=-committerdate', `--format=%(refname:short)${delimiter}%(committerdate:iso8601)${delimiter}%(objectname:short)${delimiter}%(subject)`, 'refs/heads/'],
1428
1431
  { cwd: PROJECT_ROOT }
1429
1432
  );
1430
1433
 
1431
1434
  for (const line of localOutput.split('\n').filter(Boolean)) {
1432
- const [name, dateStr, commit, subject] = line.split('|');
1435
+ const [name, dateStr, commit, ...subjectParts] = line.split(delimiter);
1436
+ const subject = subjectParts.join(delimiter);
1433
1437
  if (!seenBranches.has(name) && isValidBranchName(name)) {
1434
1438
  seenBranches.add(name);
1435
1439
  branchList.push({
@@ -1446,14 +1450,15 @@ async function getAllBranches() {
1446
1450
 
1447
1451
  // Get remote branches (using configured remote name)
1448
1452
  const remoteResult = await execGitSilent(
1449
- ['for-each-ref', '--sort=-committerdate', '--format=%(refname:short)|%(committerdate:iso8601)|%(objectname:short)|%(subject)', `refs/remotes/${REMOTE_NAME}/`],
1453
+ ['for-each-ref', '--sort=-committerdate', `--format=%(refname:short)${delimiter}%(committerdate:iso8601)${delimiter}%(objectname:short)${delimiter}%(subject)`, `refs/remotes/${REMOTE_NAME}/`],
1450
1454
  { cwd: PROJECT_ROOT }
1451
1455
  );
1452
1456
  const remoteOutput = remoteResult ? remoteResult.stdout : '';
1453
1457
 
1454
1458
  const remotePrefix = `${REMOTE_NAME}/`;
1455
1459
  for (const line of remoteOutput.split('\n').filter(Boolean)) {
1456
- const [fullName, dateStr, commit, subject] = line.split('|');
1460
+ const [fullName, dateStr, commit, ...subjectParts] = line.split(delimiter);
1461
+ const subject = subjectParts.join(delimiter);
1457
1462
  const name = fullName.replace(remotePrefix, '');
1458
1463
  if (name === 'HEAD') continue;
1459
1464
  if (!isValidBranchName(name)) continue;
@@ -1817,6 +1822,16 @@ async function pollGitChanges() {
1817
1822
  }
1818
1823
  }
1819
1824
 
1825
+ // Prune stale entries: remove branches from tracking sets/caches
1826
+ // that no longer exist in git (deleted >30s ago or already gone)
1827
+ pruneStaleEntries({
1828
+ knownBranchNames,
1829
+ fetchedBranchNames,
1830
+ allBranches,
1831
+ caches: [previousBranchStates, prInfoCache, store.get('sparklineCache'), store.get('aheadBehindCache')],
1832
+ now,
1833
+ });
1834
+
1820
1835
  // Note: isNew flag is only cleared when branch becomes current (see below)
1821
1836
 
1822
1837
  // 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.2",
3
+ "version": "1.9.4",
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
  };