git-watchtower 1.14.0 → 1.14.2

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "git-watchtower",
3
- "version": "1.14.0",
3
+ "version": "1.14.2",
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": {
@@ -198,7 +198,13 @@ async function log(branchName, options = {}) {
198
198
  }
199
199
 
200
200
  /**
201
- * Get commit count by day for sparkline
201
+ * Get commit count by day for sparkline.
202
+ *
203
+ * Buckets commits by local calendar date rather than by dividing a ms
204
+ * difference by 86 400 000, which breaks on DST transitions (a
205
+ * spring-forward day is only 23 h, causing Math.floor(23/24) = 0 and
206
+ * merging yesterday's commits into today's bucket).
207
+ *
202
208
  * @param {string} branchName - Branch name
203
209
  * @param {number} [days=7] - Number of days
204
210
  * @param {string} [cwd] - Working directory
@@ -215,15 +221,23 @@ async function getCommitsByDay(branchName, days = 7, cwd) {
215
221
 
216
222
  if (!stdout) return counts;
217
223
 
224
+ // Build a map from "YYYY-MM-DD" → bucket index. Using setDate()
225
+ // to step backwards is DST-safe because it adjusts the calendar
226
+ // day without relying on a fixed ms offset.
218
227
  const today = new Date();
219
- today.setHours(0, 0, 0, 0);
228
+ const dayBuckets = new Map();
229
+ for (let i = 0; i < days; i++) {
230
+ const d = new Date(today.getFullYear(), today.getMonth(), today.getDate() - i);
231
+ const key = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
232
+ dayBuckets.set(key, days - 1 - i);
233
+ }
220
234
 
221
235
  for (const line of stdout.split('\n').filter(Boolean)) {
222
236
  const commitDate = new Date(line);
223
- commitDate.setHours(0, 0, 0, 0);
224
- const daysDiff = Math.floor((today.getTime() - commitDate.getTime()) / (1000 * 60 * 60 * 24));
225
- if (daysDiff >= 0 && daysDiff < days) {
226
- counts[days - 1 - daysDiff]++;
237
+ const key = `${commitDate.getFullYear()}-${String(commitDate.getMonth() + 1).padStart(2, '0')}-${String(commitDate.getDate()).padStart(2, '0')}`;
238
+ const idx = dayBuckets.get(key);
239
+ if (idx !== undefined) {
240
+ counts[idx]++;
227
241
  }
228
242
  }
229
243
  } catch (error) {
@@ -265,38 +265,13 @@ class ProcessManager {
265
265
  return false;
266
266
  }
267
267
 
268
- // Capture reference before nulling — needed for deferred SIGKILL
268
+ // Capture reference before nulling — needed for deferred force-kill
269
269
  const proc = this.process;
270
270
 
271
- // Try graceful shutdown first
272
271
  if (process.platform === 'win32') {
273
- try {
274
- spawn('taskkill', ['/pid', proc.pid.toString(), '/f', '/t']);
275
- } catch (e) {
276
- // Ignore taskkill errors
277
- }
272
+ this._stopWindows(proc);
278
273
  } else {
279
- try {
280
- // Kill the entire process group (negative PID) so that
281
- // grandchildren (e.g. npm -> node -> vite) are also terminated.
282
- process.kill(-proc.pid, 'SIGTERM');
283
-
284
- // Force kill after grace period
285
- const forceKillTimeout = setTimeout(() => {
286
- try {
287
- process.kill(-proc.pid, 'SIGKILL');
288
- } catch (e) {
289
- // Process group may already be dead
290
- }
291
- }, KILL_GRACE_PERIOD);
292
-
293
- // Clear timeout if process exits cleanly
294
- proc.once('close', () => {
295
- clearTimeout(forceKillTimeout);
296
- });
297
- } catch (e) {
298
- // Process group may already be dead
299
- }
274
+ this._stopUnix(proc);
300
275
  }
301
276
 
302
277
  this.process = null;
@@ -305,6 +280,77 @@ class ProcessManager {
305
280
  return true;
306
281
  }
307
282
 
283
+ /**
284
+ * Unix stop: SIGTERM the process group, then SIGKILL after a grace period.
285
+ * The grace timer is unref'd so it doesn't keep the event loop alive when
286
+ * the main process wants to exit.
287
+ * @param {import('child_process').ChildProcess} proc
288
+ * @private
289
+ */
290
+ _stopUnix(proc) {
291
+ // If the process has already exited, there's nothing to signal.
292
+ if (proc.exitCode !== null || proc.signalCode !== null) return;
293
+
294
+ try {
295
+ process.kill(-proc.pid, 'SIGTERM');
296
+ } catch (e) {
297
+ // Process group may already be dead
298
+ return;
299
+ }
300
+
301
+ const forceKillTimeout = setTimeout(() => {
302
+ // Re-check: process may have exited during the grace period.
303
+ if (proc.exitCode !== null || proc.signalCode !== null) return;
304
+ try {
305
+ process.kill(-proc.pid, 'SIGKILL');
306
+ } catch (e) {
307
+ // Process group may already be dead
308
+ }
309
+ }, KILL_GRACE_PERIOD);
310
+
311
+ // Don't let this timer keep the event loop alive on shutdown.
312
+ forceKillTimeout.unref();
313
+
314
+ // Clear early if the process exits before the grace period.
315
+ proc.once('close', () => {
316
+ clearTimeout(forceKillTimeout);
317
+ });
318
+ }
319
+
320
+ /**
321
+ * Windows stop: taskkill /t (tree kill). If the process doesn't exit
322
+ * within the grace period, retry with /f (force).
323
+ * @param {import('child_process').ChildProcess} proc
324
+ * @private
325
+ */
326
+ _stopWindows(proc) {
327
+ if (proc.exitCode !== null || proc.signalCode !== null) return;
328
+
329
+ try {
330
+ spawn('taskkill', ['/pid', proc.pid.toString(), '/t']);
331
+ } catch (e) {
332
+ // Ignore spawn errors (PID already gone, etc.)
333
+ return;
334
+ }
335
+
336
+ // Fallback: force-kill if the process is still alive after the
337
+ // grace period. This mirrors the Unix SIGTERM → SIGKILL pattern.
338
+ const forceKillTimeout = setTimeout(() => {
339
+ if (proc.exitCode !== null || proc.signalCode !== null) return;
340
+ try {
341
+ spawn('taskkill', ['/pid', proc.pid.toString(), '/f', '/t']);
342
+ } catch (e) {
343
+ // Ignore — process may already be dead
344
+ }
345
+ }, KILL_GRACE_PERIOD);
346
+
347
+ forceKillTimeout.unref();
348
+
349
+ proc.once('close', () => {
350
+ clearTimeout(forceKillTimeout);
351
+ });
352
+ }
353
+
308
354
  /**
309
355
  * Restart the server process
310
356
  * @returns {Promise<{success: boolean, error?: Error, pid?: number}>}