git-watchtower 2.3.17 → 2.3.19

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.
@@ -1881,18 +1881,24 @@ async function pollGitChanges() {
1881
1881
  // Skip if a poll is already in progress (don't queue)
1882
1882
  if (pollMutex.isLocked()) return;
1883
1883
  const pollToken = await pollMutex.acquire();
1884
- store.setState({ isPolling: true, pollingStatus: 'fetching' });
1885
1884
 
1886
- // Casino mode: start slot reels spinning (no sound - too annoying)
1887
- if (store.get('casinoModeEnabled')) {
1888
- casino.startSlotReels(render);
1889
- }
1885
+ // Everything past acquire() must be wrapped so the finally releases
1886
+ // the token. The previous shape kept the setState / casino.startSlotReels
1887
+ // / render() calls outside the try, so a throw from any of them (e.g. a
1888
+ // store middleware error, a casino interval-setup failure) leaked the
1889
+ // mutex permanently and stalled every subsequent poll cycle.
1890
+ try {
1891
+ store.setState({ isPolling: true, pollingStatus: 'fetching' });
1890
1892
 
1891
- render();
1893
+ // Casino mode: start slot reels spinning (no sound - too annoying)
1894
+ if (store.get('casinoModeEnabled')) {
1895
+ casino.startSlotReels(render);
1896
+ }
1892
1897
 
1893
- const fetchStartTime = Date.now();
1898
+ render();
1899
+
1900
+ const fetchStartTime = Date.now();
1894
1901
 
1895
- try {
1896
1902
  const newCurrentBranch = await getCurrentBranch();
1897
1903
  const prevCurrentBranch = store.get('currentBranch');
1898
1904
 
@@ -2004,12 +2010,10 @@ async function pollGitChanges() {
2004
2010
  const updatedBranches = [];
2005
2011
  const updatedBranchPrevCommits = new Map();
2006
2012
  const currentBranchName = store.get('currentBranch');
2007
- const activeBranchNames = new Set();
2008
2013
  for (const branch of pollFilteredBranches) {
2009
2014
  // Clear previous cycle's flag so only freshly-updated branches are highlighted
2010
2015
  branch.justUpdated = false;
2011
2016
  if (branch.isDeleted) continue;
2012
- activeBranchNames.add(branch.name);
2013
2017
  const prevCommit = previousBranchStates.get(branch.name);
2014
2018
  if (prevCommit && prevCommit !== branch.commit && branch.name !== currentBranchName) {
2015
2019
  updatedBranches.push(branch);
@@ -2019,17 +2023,13 @@ async function pollGitChanges() {
2019
2023
  previousBranchStates.set(branch.name, branch.commit);
2020
2024
  }
2021
2025
 
2022
- // Remove stale entries from caches for branches
2023
- // that no longer exist in the current poll results
2024
- const staleCaches = [previousBranchStates, prInfoCache, store.get('sparklineCache'), store.get('aheadBehindCache')];
2025
- for (const cache of staleCaches) {
2026
- if (!cache) continue;
2027
- for (const name of cache.keys()) {
2028
- if (!activeBranchNames.has(name)) {
2029
- cache.delete(name);
2030
- }
2031
- }
2032
- }
2026
+ // (No second prune pass here pruneStaleEntries above already prunes
2027
+ // these four caches against fetchedBranchNames with a 30 s deleted-
2028
+ // branch grace period. The previous extra loop pruned against the
2029
+ // active-only set instead, which excluded isDeleted entries and so
2030
+ // wiped a recently-deleted branch's sparkline / PR status / ahead-
2031
+ // behind data IMMEDIATELY contradicting the retention window the
2032
+ // first prune was specifically designed to provide.)
2033
2033
 
2034
2034
  // Flash and sound for updates or new branches
2035
2035
  const casinoOn = store.get('casinoModeEnabled');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "git-watchtower",
3
- "version": "2.3.17",
3
+ "version": "2.3.19",
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": {
@@ -108,6 +108,24 @@ function ensureDir() {
108
108
 
109
109
  /**
110
110
  * Check if a process with the given PID is alive.
111
+ *
112
+ * `process.kill(pid, 0)` is the standard "is this PID alive?" probe. It
113
+ * sends signal 0 (no-op) and surfaces the kernel's answer via errno:
114
+ *
115
+ * - resolves cleanly → process exists and we can signal it
116
+ * - throws ESRCH → no such process; safe to call dead
117
+ * - throws EPERM → process exists but is owned by another
118
+ * user or in a different cgroup. STILL
119
+ * alive — we just can't signal it.
120
+ *
121
+ * Treating EPERM as "dead" was a real bug for the coordinator lock: if
122
+ * a coordinator's PID was reused by another local user's process after
123
+ * a crash, a peer instance would read the lock, see EPERM, decide the
124
+ * coordinator was dead, unlink the lock, and try to take over while
125
+ * the original (or reused) PID was still running. Mirroring the same
126
+ * EPERM-aware check used by src/utils/monitor-lock.js prevents that
127
+ * cleanup-then-clobber race.
128
+ *
111
129
  * @param {number} pid
112
130
  * @returns {boolean}
113
131
  */
@@ -116,7 +134,8 @@ function isProcessAlive(pid) {
116
134
  process.kill(pid, 0);
117
135
  return true;
118
136
  } catch (e) {
119
- return false;
137
+ // ESRCH = no such process; EPERM = exists but owned by another user.
138
+ return e.code === 'EPERM';
120
139
  }
121
140
  }
122
141