@gtrabanco/pi-web-github-pr-status 0.1.0 → 0.1.1
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/CHANGELOG.md +4 -0
- package/dist/index.js +169 -5
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project are documented here. The project follows [strict semantic versioning](./RELEASE-POLICY.md); the kind of the next release is registered in `package.json` (`nextRelease`) before `bun run publish` is executed.
|
|
4
4
|
|
|
5
|
+
## 0.1.1 — 2026-09-08 (Patch release)
|
|
6
|
+
|
|
7
|
+
- See commit history for details.
|
|
8
|
+
|
|
5
9
|
## 0.1.0 — initial development version
|
|
6
10
|
|
|
7
11
|
- GitHub PR workspace label: PR number link, CI ball (green/orange/red/none), dirty-worktree marker, push/pull arrows.
|
package/dist/index.js
CHANGED
|
@@ -299,12 +299,15 @@ async function readAllProbeFiles(readFile) {
|
|
|
299
299
|
var DEFAULT_SETTINGS = Object.freeze({
|
|
300
300
|
showCI: true,
|
|
301
301
|
refreshSeconds: 90,
|
|
302
|
+
adaptiveRefresh: true,
|
|
302
303
|
merge: Object.freeze({
|
|
303
304
|
enabled: true,
|
|
304
305
|
method: "merge",
|
|
305
306
|
requireCleanWorktree: true,
|
|
306
307
|
requireCI: true,
|
|
307
|
-
deleteBranch: false
|
|
308
|
+
deleteBranch: false,
|
|
309
|
+
checkoutTarget: true,
|
|
310
|
+
deleteBranchAfterMerge: true
|
|
308
311
|
})
|
|
309
312
|
});
|
|
310
313
|
var MERGE_METHODS = ["merge", "squash", "rebase"];
|
|
@@ -341,6 +344,8 @@ function mergeBooleans(raw, defaults, warnings, prefix) {
|
|
|
341
344
|
requireCleanWorktree: pick("requireCleanWorktree"),
|
|
342
345
|
requireCI: pick("requireCI"),
|
|
343
346
|
deleteBranch: pick("deleteBranch"),
|
|
347
|
+
checkoutTarget: pick("checkoutTarget"),
|
|
348
|
+
deleteBranchAfterMerge: pick("deleteBranchAfterMerge"),
|
|
344
349
|
method: normalizeMethod(raw.method, defaults.method, warnings)
|
|
345
350
|
};
|
|
346
351
|
}
|
|
@@ -369,10 +374,14 @@ function normalizeSettings(raw) {
|
|
|
369
374
|
const showCI = coerceBoolean(source.showCI);
|
|
370
375
|
if (showCI === undefined && source.showCI !== undefined)
|
|
371
376
|
warnings.push("showCI: ignored invalid value");
|
|
377
|
+
const adaptiveRefresh = coerceBoolean(source.adaptiveRefresh);
|
|
378
|
+
if (adaptiveRefresh === undefined && source.adaptiveRefresh !== undefined)
|
|
379
|
+
warnings.push("adaptiveRefresh: ignored invalid value");
|
|
372
380
|
const mergeSource = asObject(source.merge);
|
|
373
381
|
const settings = {
|
|
374
382
|
showCI: showCI ?? DEFAULT_SETTINGS.showCI,
|
|
375
383
|
refreshSeconds: normalizeRefreshSeconds(source.refreshSeconds, DEFAULT_SETTINGS.refreshSeconds, warnings),
|
|
384
|
+
adaptiveRefresh: adaptiveRefresh ?? DEFAULT_SETTINGS.adaptiveRefresh,
|
|
376
385
|
merge: mergeBooleans(mergeSource, DEFAULT_SETTINGS.merge, warnings, "merge.")
|
|
377
386
|
};
|
|
378
387
|
return { settings, warnings };
|
|
@@ -617,6 +626,20 @@ function buildCloseCommand(prNumber) {
|
|
|
617
626
|
assertPrNumber(prNumber);
|
|
618
627
|
return `gh pr close ${String(prNumber)} < /dev/null`;
|
|
619
628
|
}
|
|
629
|
+
function buildCheckoutTargetCommand(targetBranch) {
|
|
630
|
+
if (typeof targetBranch !== "string" || targetBranch.trim() === "") {
|
|
631
|
+
throw new Error("Invalid target branch name");
|
|
632
|
+
}
|
|
633
|
+
const safe = targetBranch.replace(/[^a-zA-Z0-9_./-]/g, "");
|
|
634
|
+
return `git checkout ${safe} && git pull origin ${safe}`;
|
|
635
|
+
}
|
|
636
|
+
function buildDeleteLocalBranchCommand(branchName) {
|
|
637
|
+
if (typeof branchName !== "string" || branchName.trim() === "") {
|
|
638
|
+
throw new Error("Invalid branch name");
|
|
639
|
+
}
|
|
640
|
+
const safe = branchName.replace(/[^a-zA-Z0-9_./-]/g, "");
|
|
641
|
+
return `git branch -d ${safe}`;
|
|
642
|
+
}
|
|
620
643
|
function explainCiState(ci) {
|
|
621
644
|
switch (ci.state) {
|
|
622
645
|
case "none":
|
|
@@ -653,7 +676,8 @@ class PrUiController {
|
|
|
653
676
|
outcome: null,
|
|
654
677
|
settingsOpen: false,
|
|
655
678
|
draft: null,
|
|
656
|
-
lastInvalidate: 0
|
|
679
|
+
lastInvalidate: 0,
|
|
680
|
+
postMerge: null
|
|
657
681
|
};
|
|
658
682
|
this.states.set(key, created);
|
|
659
683
|
return created;
|
|
@@ -681,7 +705,7 @@ class PrUiController {
|
|
|
681
705
|
if (settings.refreshSeconds <= 0)
|
|
682
706
|
return;
|
|
683
707
|
const entry = statusCache.get(context);
|
|
684
|
-
const staleMs = settings
|
|
708
|
+
const staleMs = this.effectiveRefreshMs(settings, entry?.status);
|
|
685
709
|
if (entry === undefined) {
|
|
686
710
|
this.probe(context);
|
|
687
711
|
return;
|
|
@@ -689,6 +713,17 @@ class PrUiController {
|
|
|
689
713
|
if (Date.now() - Math.max(entry.probedAt, entry.loadedAt) >= staleMs)
|
|
690
714
|
this.probe(context);
|
|
691
715
|
}
|
|
716
|
+
effectiveRefreshMs(settings, status) {
|
|
717
|
+
const baseMs = settings.refreshSeconds * 1000;
|
|
718
|
+
if (!settings.adaptiveRefresh || status === undefined)
|
|
719
|
+
return baseMs;
|
|
720
|
+
const ci = status.ci;
|
|
721
|
+
if (ci.state === "running")
|
|
722
|
+
return Math.min(baseMs, 20000);
|
|
723
|
+
if (ci.state === "failed")
|
|
724
|
+
return Math.min(baseMs, 45000);
|
|
725
|
+
return baseMs;
|
|
726
|
+
}
|
|
692
727
|
invalidate(context) {
|
|
693
728
|
const state = this.stateFor(context);
|
|
694
729
|
const now = Date.now();
|
|
@@ -769,6 +804,7 @@ class PrUiController {
|
|
|
769
804
|
state.confirm = null;
|
|
770
805
|
state.busy = "merge";
|
|
771
806
|
state.outcome = null;
|
|
807
|
+
state.postMerge = null;
|
|
772
808
|
this.requestRender(state);
|
|
773
809
|
try {
|
|
774
810
|
const run = await runGuardedCommand(context, {
|
|
@@ -778,6 +814,11 @@ class PrUiController {
|
|
|
778
814
|
});
|
|
779
815
|
if (run.exitCode === 0) {
|
|
780
816
|
state.outcome = { ok: true, message: `PR #${String(pr.number)} merged (${settings.merge.method}).` };
|
|
817
|
+
const targetBranch = pr.baseRefName;
|
|
818
|
+
const mergedBranch = pr.headRefName ?? status?.branch;
|
|
819
|
+
if (settings.merge.checkoutTarget && targetBranch !== undefined && mergedBranch !== undefined) {
|
|
820
|
+
state.postMerge = { targetBranch, mergedBranch };
|
|
821
|
+
}
|
|
781
822
|
} else {
|
|
782
823
|
state.outcome = { ok: false, message: `Merge failed (exit ${String(run.exitCode ?? "?")}). Check the terminal output.`, terminalId: run.terminalId };
|
|
783
824
|
}
|
|
@@ -791,6 +832,71 @@ class PrUiController {
|
|
|
791
832
|
this.requestRender(state);
|
|
792
833
|
}
|
|
793
834
|
}
|
|
835
|
+
async postMergeCheckout(context) {
|
|
836
|
+
const state = this.stateFor(context);
|
|
837
|
+
const pm = state.postMerge;
|
|
838
|
+
if (pm === null)
|
|
839
|
+
return;
|
|
840
|
+
state.busy = "post-merge";
|
|
841
|
+
state.outcome = null;
|
|
842
|
+
this.requestRender(state);
|
|
843
|
+
try {
|
|
844
|
+
const run = await runGuardedCommand(context, {
|
|
845
|
+
title: `Checkout ${pm.targetBranch}`,
|
|
846
|
+
command: buildCheckoutTargetCommand(pm.targetBranch),
|
|
847
|
+
op: "post-merge"
|
|
848
|
+
});
|
|
849
|
+
if (run.exitCode === 0) {
|
|
850
|
+
state.outcome = { ok: true, message: `Switched to ${pm.targetBranch} and pulled latest.` };
|
|
851
|
+
state.postMerge = null;
|
|
852
|
+
} else {
|
|
853
|
+
state.outcome = { ok: false, message: `Checkout failed (exit ${String(run.exitCode ?? "?")}).`, terminalId: run.terminalId };
|
|
854
|
+
}
|
|
855
|
+
} catch (error) {
|
|
856
|
+
state.outcome = { ok: false, message: errorMessage(error) };
|
|
857
|
+
} finally {
|
|
858
|
+
state.busy = null;
|
|
859
|
+
statusCache.probe(context, { force: true }).catch(() => {
|
|
860
|
+
return;
|
|
861
|
+
});
|
|
862
|
+
this.requestRender(state);
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
async postMergeDeleteBranch(context) {
|
|
866
|
+
const state = this.stateFor(context);
|
|
867
|
+
const pm = state.postMerge;
|
|
868
|
+
if (pm === null)
|
|
869
|
+
return;
|
|
870
|
+
state.busy = "post-merge";
|
|
871
|
+
state.outcome = null;
|
|
872
|
+
this.requestRender(state);
|
|
873
|
+
try {
|
|
874
|
+
const run = await runGuardedCommand(context, {
|
|
875
|
+
title: `Delete branch ${pm.mergedBranch}`,
|
|
876
|
+
command: buildDeleteLocalBranchCommand(pm.mergedBranch),
|
|
877
|
+
op: "post-merge"
|
|
878
|
+
});
|
|
879
|
+
if (run.exitCode === 0) {
|
|
880
|
+
state.outcome = { ok: true, message: `Branch ${pm.mergedBranch} deleted.` };
|
|
881
|
+
state.postMerge = null;
|
|
882
|
+
} else {
|
|
883
|
+
state.outcome = { ok: false, message: `Delete failed (exit ${String(run.exitCode ?? "?")}).`, terminalId: run.terminalId };
|
|
884
|
+
}
|
|
885
|
+
} catch (error) {
|
|
886
|
+
state.outcome = { ok: false, message: errorMessage(error) };
|
|
887
|
+
} finally {
|
|
888
|
+
state.busy = null;
|
|
889
|
+
statusCache.probe(context, { force: true }).catch(() => {
|
|
890
|
+
return;
|
|
891
|
+
});
|
|
892
|
+
this.requestRender(state);
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
dismissPostMerge(context) {
|
|
896
|
+
const state = this.stateFor(context);
|
|
897
|
+
state.postMerge = null;
|
|
898
|
+
this.requestRender(state);
|
|
899
|
+
}
|
|
794
900
|
async runClose(context, status) {
|
|
795
901
|
const state = this.stateFor(context);
|
|
796
902
|
const pr = status?.pr;
|
|
@@ -985,7 +1091,8 @@ function renderBody(html, controller, context, state, settings, status, busy) {
|
|
|
985
1091
|
${settings.showCI ? renderCiSection(html, status) : html`<p class="ghpr-muted">CI display is disabled in settings.</p>`}
|
|
986
1092
|
${renderWorktreeSection(html, status)}
|
|
987
1093
|
${renderOutcome(html, state)}
|
|
988
|
-
${
|
|
1094
|
+
${state.postMerge !== null ? renderPostMerge(html, controller, context, state, busy) : null}
|
|
1095
|
+
${pr !== undefined && pr.state === "OPEN" && state.postMerge === null ? renderActions(html, controller, context, state, mergeEvaluation, closeEvaluation, busy) : null}
|
|
989
1096
|
${renderSettings(html, controller, context, state, settings)}
|
|
990
1097
|
`;
|
|
991
1098
|
}
|
|
@@ -1078,6 +1185,48 @@ function renderOutcome(html, state) {
|
|
|
1078
1185
|
</div>
|
|
1079
1186
|
`;
|
|
1080
1187
|
}
|
|
1188
|
+
function renderPostMerge(html, controller, context, state, busy) {
|
|
1189
|
+
const pm = state.postMerge;
|
|
1190
|
+
if (pm === null)
|
|
1191
|
+
return null;
|
|
1192
|
+
return html`
|
|
1193
|
+
<section class="ghpr-actions ghpr-post-merge">
|
|
1194
|
+
<p class="ghpr-post-merge-title">Merge complete! Next steps:</p>
|
|
1195
|
+
<div class="ghpr-buttons">
|
|
1196
|
+
<button
|
|
1197
|
+
type="button"
|
|
1198
|
+
class="ghpr-primary"
|
|
1199
|
+
title="Checkout "${pm.targetBranch}" and pull latest changes"
|
|
1200
|
+
?disabled=${busy}
|
|
1201
|
+
@click=${() => {
|
|
1202
|
+
controller.postMergeCheckout(context);
|
|
1203
|
+
}}
|
|
1204
|
+
>
|
|
1205
|
+
${state.busy === "post-merge" ? "Working…" : `Checkout ${pm.targetBranch} + pull`}
|
|
1206
|
+
</button>
|
|
1207
|
+
<button
|
|
1208
|
+
type="button"
|
|
1209
|
+
title="Delete the merged branch "${pm.mergedBranch}" locally"
|
|
1210
|
+
?disabled=${busy}
|
|
1211
|
+
@click=${() => {
|
|
1212
|
+
controller.postMergeDeleteBranch(context);
|
|
1213
|
+
}}
|
|
1214
|
+
>
|
|
1215
|
+
${state.busy === "post-merge" ? "Working…" : `Delete branch ${pm.mergedBranch}`}
|
|
1216
|
+
</button>
|
|
1217
|
+
<button
|
|
1218
|
+
type="button"
|
|
1219
|
+
?disabled=${busy}
|
|
1220
|
+
@click=${() => {
|
|
1221
|
+
controller.dismissPostMerge(context);
|
|
1222
|
+
}}
|
|
1223
|
+
>
|
|
1224
|
+
Dismiss
|
|
1225
|
+
</button>
|
|
1226
|
+
</div>
|
|
1227
|
+
</section>
|
|
1228
|
+
`;
|
|
1229
|
+
}
|
|
1081
1230
|
function renderActions(html, controller, context, state, mergeEvaluation, closeEvaluation, busy) {
|
|
1082
1231
|
const mergeTitle = mergeEvaluation.canMerge ? mergeEvaluation.confirmations.length > 0 ? "Merge with confirmation" : "Merge this pull request" : mergeEvaluation.blockers.join(" · ");
|
|
1083
1232
|
const confirm = state.confirm;
|
|
@@ -1170,8 +1319,23 @@ function renderSettings(html, controller, context, state, settings) {
|
|
|
1170
1319
|
d.merge.deleteBranch = value;
|
|
1171
1320
|
});
|
|
1172
1321
|
}} /> Delete
|
|
1173
|
-
branch after merge</label
|
|
1322
|
+
branch after merge (remote)</label
|
|
1174
1323
|
>
|
|
1324
|
+
<label><input type="checkbox" ?checked=${draft.merge.checkoutTarget} @change=${(event) => {
|
|
1325
|
+
updateCheckbox(context, controller, event, (d, value) => {
|
|
1326
|
+
d.merge.checkoutTarget = value;
|
|
1327
|
+
});
|
|
1328
|
+
}} /> Offer checkout to target branch after merge</label>
|
|
1329
|
+
<label><input type="checkbox" ?checked=${draft.merge.deleteBranchAfterMerge} @change=${(event) => {
|
|
1330
|
+
updateCheckbox(context, controller, event, (d, value) => {
|
|
1331
|
+
d.merge.deleteBranchAfterMerge = value;
|
|
1332
|
+
});
|
|
1333
|
+
}} /> Offer delete merged branch after merge</label>
|
|
1334
|
+
<label><input type="checkbox" ?checked=${draft.adaptiveRefresh} @change=${(event) => {
|
|
1335
|
+
updateCheckbox(context, controller, event, (d, value) => {
|
|
1336
|
+
d.adaptiveRefresh = value;
|
|
1337
|
+
});
|
|
1338
|
+
}} /> Adaptive refresh (faster when CI is running)</label>
|
|
1175
1339
|
<label>
|
|
1176
1340
|
Merge method
|
|
1177
1341
|
<select
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gtrabanco/pi-web-github-pr-status",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"nextRelease": "patch",
|
|
5
5
|
"description": "PI WEB plugin that surfaces GitHub pull request status (PR number, link, CI ball, worktree state) and one-click guarded merge/close without leaving the workspace.",
|
|
6
6
|
"license": "MIT",
|