git-watchtower 2.3.0 → 2.3.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/bin/git-watchtower.js +85 -0
- package/package.json +1 -1
- package/src/server/web.js +36 -14
package/bin/git-watchtower.js
CHANGED
|
@@ -3340,6 +3340,84 @@ async function handleWebAction(action, payload) {
|
|
|
3340
3340
|
});
|
|
3341
3341
|
}
|
|
3342
3342
|
break;
|
|
3343
|
+
case 'stash': {
|
|
3344
|
+
// If a switch/pull is queued behind a dirty working tree, run the
|
|
3345
|
+
// existing stash-and-retry flow so the original operation completes
|
|
3346
|
+
// after the stash. Otherwise just stash standalone.
|
|
3347
|
+
if (pendingDirtyOperation) {
|
|
3348
|
+
const op = pendingDirtyOperation;
|
|
3349
|
+
await stashAndRetry();
|
|
3350
|
+
const label = op.type === 'switch' ? `stashed and switched to ${op.branch}` : 'stashed and pulled';
|
|
3351
|
+
sendResult(true, label);
|
|
3352
|
+
} else {
|
|
3353
|
+
const stashResult = await gitStash({ message: 'git-watchtower: stashed from web dashboard' });
|
|
3354
|
+
if (stashResult.success) {
|
|
3355
|
+
addLog('Changes stashed (from web)', 'success');
|
|
3356
|
+
telemetry.capture('stash_performed');
|
|
3357
|
+
await pollGitChanges();
|
|
3358
|
+
sendResult(true, 'Changes stashed');
|
|
3359
|
+
} else {
|
|
3360
|
+
const msg = stashResult.error ? stashResult.error.message : 'Could not stash';
|
|
3361
|
+
addLog(`Stash failed (from web): ${msg}`, 'error');
|
|
3362
|
+
sendResult(false, msg);
|
|
3363
|
+
}
|
|
3364
|
+
render();
|
|
3365
|
+
}
|
|
3366
|
+
break;
|
|
3367
|
+
}
|
|
3368
|
+
case 'stashPop': {
|
|
3369
|
+
const popResult = await gitStashPop();
|
|
3370
|
+
if (popResult.success) {
|
|
3371
|
+
addLog('Stash popped (from web)', 'success');
|
|
3372
|
+
await pollGitChanges();
|
|
3373
|
+
sendResult(true, 'Stash popped');
|
|
3374
|
+
} else {
|
|
3375
|
+
const msg = popResult.error ? popResult.error.message : 'Could not pop stash';
|
|
3376
|
+
addLog(`Stash pop failed (from web): ${msg}`, 'error');
|
|
3377
|
+
sendResult(false, msg);
|
|
3378
|
+
}
|
|
3379
|
+
render();
|
|
3380
|
+
break;
|
|
3381
|
+
}
|
|
3382
|
+
case 'deleteBranches': {
|
|
3383
|
+
const branches = Array.isArray(payload.branches) ? payload.branches : [];
|
|
3384
|
+
const force = payload.force === true;
|
|
3385
|
+
if (branches.length === 0) {
|
|
3386
|
+
sendResult(false, 'No branches specified');
|
|
3387
|
+
break;
|
|
3388
|
+
}
|
|
3389
|
+
addLog(
|
|
3390
|
+
`Cleaning up ${branches.length} branch${branches.length === 1 ? '' : 'es'}${force ? ' (force)' : ''} (from web)...`,
|
|
3391
|
+
'update'
|
|
3392
|
+
);
|
|
3393
|
+
render();
|
|
3394
|
+
const cleanupResult = await deleteGoneBranches(branches, { force });
|
|
3395
|
+
for (const name of cleanupResult.deleted) {
|
|
3396
|
+
addLog(`Deleted branch: ${name}`, 'success');
|
|
3397
|
+
}
|
|
3398
|
+
for (const f of cleanupResult.failed) {
|
|
3399
|
+
addLog(`Failed to delete ${f.name}: ${f.error}`, 'error');
|
|
3400
|
+
}
|
|
3401
|
+
if (cleanupResult.deleted.length > 0) {
|
|
3402
|
+
telemetry.capture('cleanup_branches_deleted', { count: cleanupResult.deleted.length });
|
|
3403
|
+
await pollGitChanges();
|
|
3404
|
+
}
|
|
3405
|
+
if (cleanupResult.failed.length === 0 && cleanupResult.deleted.length > 0) {
|
|
3406
|
+
sendResult(true, `Deleted ${cleanupResult.deleted.length} branch${cleanupResult.deleted.length === 1 ? '' : 'es'}`);
|
|
3407
|
+
} else if (cleanupResult.deleted.length > 0) {
|
|
3408
|
+
sendResult(
|
|
3409
|
+
false,
|
|
3410
|
+
`Deleted ${cleanupResult.deleted.length}, failed ${cleanupResult.failed.length}`
|
|
3411
|
+
);
|
|
3412
|
+
} else {
|
|
3413
|
+
sendResult(
|
|
3414
|
+
false,
|
|
3415
|
+
`Failed to delete ${cleanupResult.failed.length} branch${cleanupResult.failed.length === 1 ? '' : 'es'}`
|
|
3416
|
+
);
|
|
3417
|
+
}
|
|
3418
|
+
render();
|
|
3419
|
+
break;
|
|
3420
|
+
}
|
|
3343
3421
|
}
|
|
3344
3422
|
} catch (err) {
|
|
3345
3423
|
addLog(`Web action error: ${err.message}`, 'error');
|
|
@@ -3407,6 +3485,13 @@ async function startWebDashboard(openBrowser) {
|
|
|
3407
3485
|
sessionStats: sessionStats.getStats(),
|
|
3408
3486
|
}),
|
|
3409
3487
|
onAction: handleWebAction,
|
|
3488
|
+
// Route actions for non-local project tabs through the coordinator so
|
|
3489
|
+
// the targeted worker handles them in its own process. Without this,
|
|
3490
|
+
// every action runs against the coordinator's repo regardless of which
|
|
3491
|
+
// tab the user clicked.
|
|
3492
|
+
sendCommand: (pId, action, payload) => {
|
|
3493
|
+
if (coordinator) coordinator.sendCommand(pId, action, payload);
|
|
3494
|
+
},
|
|
3410
3495
|
});
|
|
3411
3496
|
webDashboard.setLocalProjectId(projectId);
|
|
3412
3497
|
|
package/package.json
CHANGED
package/src/server/web.js
CHANGED
|
@@ -36,11 +36,29 @@ const MAX_PORT_RETRIES = 20;
|
|
|
36
36
|
*/
|
|
37
37
|
const SSE_KEEPALIVE_INTERVAL = 15000;
|
|
38
38
|
|
|
39
|
+
/**
|
|
40
|
+
* Actions the web dashboard is allowed to POST to /api/action. Every entry
|
|
41
|
+
* here MUST be matched by a `case` in `handleWebAction` in bin/git-watchtower.js
|
|
42
|
+
* — `tests/unit/server/web.test.js` enforces that link so a future addition
|
|
43
|
+
* to the whitelist can't silently no-op the way `stash` / `stashPop` /
|
|
44
|
+
* `deleteBranches` did before they were implemented.
|
|
45
|
+
*/
|
|
46
|
+
const ALLOWED_ACTIONS = Object.freeze([
|
|
47
|
+
'switchBranch', 'pull', 'fetch', 'undo',
|
|
48
|
+
'toggleSound', 'preview',
|
|
49
|
+
'restartServer', 'reloadBrowsers', 'toggleCasino',
|
|
50
|
+
'openBrowser',
|
|
51
|
+
'stash', 'stashPop', 'deleteBranches', 'checkUpdate',
|
|
52
|
+
]);
|
|
53
|
+
|
|
39
54
|
/**
|
|
40
55
|
* @typedef {Object} WebDashboardOptions
|
|
41
56
|
* @property {number} [port=4000] - Port to listen on
|
|
42
57
|
* @property {import('../state/store').Store} store - State store instance
|
|
43
|
-
* @property {function} [onAction] - Callback for
|
|
58
|
+
* @property {function} [onAction] - Callback for actions targeting the local project
|
|
59
|
+
* @property {function} [sendCommand] - Routes (projectId, action, payload) to a remote
|
|
60
|
+
* project's worker. When omitted, every action is dispatched locally — preserves
|
|
61
|
+
* single-project behaviour for callers (and tests) that don't run a coordinator.
|
|
44
62
|
* @property {function} [getExtraState] - Returns additional state to merge
|
|
45
63
|
*/
|
|
46
64
|
|
|
@@ -56,6 +74,7 @@ class WebDashboardServer {
|
|
|
56
74
|
this.port = options.port || DEFAULT_WEB_PORT;
|
|
57
75
|
this.store = options.store;
|
|
58
76
|
this.onAction = options.onAction || (() => {});
|
|
77
|
+
this.sendCommand = options.sendCommand || null;
|
|
59
78
|
this.getExtraState = options.getExtraState || (() => ({}));
|
|
60
79
|
|
|
61
80
|
/** @type {Set<import('http').ServerResponse>} */
|
|
@@ -511,27 +530,29 @@ class WebDashboardServer {
|
|
|
511
530
|
return;
|
|
512
531
|
}
|
|
513
532
|
|
|
514
|
-
|
|
515
|
-
const allowedActions = [
|
|
516
|
-
'switchBranch', 'pull', 'fetch', 'undo',
|
|
517
|
-
'toggleSound', 'preview',
|
|
518
|
-
'restartServer', 'reloadBrowsers', 'toggleCasino',
|
|
519
|
-
'openBrowser',
|
|
520
|
-
'stash', 'stashPop', 'deleteBranches', 'checkUpdate',
|
|
521
|
-
];
|
|
522
|
-
|
|
523
|
-
if (!allowedActions.includes(action)) {
|
|
533
|
+
if (!ALLOWED_ACTIONS.includes(action)) {
|
|
524
534
|
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
525
535
|
res.end(JSON.stringify({ error: 'Unknown action: ' + action }));
|
|
526
536
|
return;
|
|
527
537
|
}
|
|
528
538
|
|
|
529
|
-
//
|
|
539
|
+
// Multi-project routing: when the request targets a project that is
|
|
540
|
+
// not the coordinator's own (the user is on a different tab in the
|
|
541
|
+
// dashboard), forward to that project's worker via sendCommand
|
|
542
|
+
// instead of running the action against the coordinator's repo.
|
|
530
543
|
const projectId = data.projectId || this.localProjectId;
|
|
531
544
|
payload._projectId = projectId;
|
|
532
545
|
|
|
533
|
-
|
|
534
|
-
|
|
546
|
+
if (
|
|
547
|
+
projectId &&
|
|
548
|
+
this.localProjectId &&
|
|
549
|
+
projectId !== this.localProjectId &&
|
|
550
|
+
typeof this.sendCommand === 'function'
|
|
551
|
+
) {
|
|
552
|
+
this.sendCommand(projectId, action, payload);
|
|
553
|
+
} else {
|
|
554
|
+
this.onAction(action, payload);
|
|
555
|
+
}
|
|
535
556
|
|
|
536
557
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
537
558
|
res.end(JSON.stringify({ ok: true }));
|
|
@@ -581,4 +602,5 @@ module.exports = {
|
|
|
581
602
|
WebDashboardServer,
|
|
582
603
|
DEFAULT_WEB_PORT,
|
|
583
604
|
STATE_PUSH_INTERVAL,
|
|
605
|
+
ALLOWED_ACTIONS,
|
|
584
606
|
};
|