git-coco 0.46.0 → 0.47.0
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/dist/index.esm.mjs +108 -18
- package/dist/index.js +108 -18
- package/package.json +1 -1
package/dist/index.esm.mjs
CHANGED
|
@@ -54,7 +54,7 @@ import { pathToFileURL } from 'url';
|
|
|
54
54
|
/**
|
|
55
55
|
* Current build version from package.json
|
|
56
56
|
*/
|
|
57
|
-
const BUILD_VERSION = "0.
|
|
57
|
+
const BUILD_VERSION = "0.47.0";
|
|
58
58
|
|
|
59
59
|
const isInteractive = (config) => {
|
|
60
60
|
return config?.mode === 'interactive' || !!config?.interactive;
|
|
@@ -26188,6 +26188,35 @@ function LogInkApp(deps) {
|
|
|
26188
26188
|
})();
|
|
26189
26189
|
return () => { active = false; };
|
|
26190
26190
|
}, [git, state.activeView, state.diffSource, state.stashDiffRef]);
|
|
26191
|
+
// #879 (item 2) — load commit detail for the active bisect's
|
|
26192
|
+
// current candidate so the bisect surface can show "what changed
|
|
26193
|
+
// here" alongside the decision keys. Mirrors the history-detail
|
|
26194
|
+
// loader's shape but keyed on `bisect.currentSha` and only fires
|
|
26195
|
+
// when the bisect view is active. Best-effort: any failure leaves
|
|
26196
|
+
// the surface in its non-detail mode (decision log only) — never
|
|
26197
|
+
// crash the workstation because git couldn't resolve a sha.
|
|
26198
|
+
const [bisectCandidateDetail, setBisectCandidateDetail] = React.useState(undefined);
|
|
26199
|
+
const [bisectCandidateLoading, setBisectCandidateLoading] = React.useState(false);
|
|
26200
|
+
const bisectCandidateSha = state.activeView === 'bisect' && context.bisect?.active
|
|
26201
|
+
? context.bisect.currentSha
|
|
26202
|
+
: '';
|
|
26203
|
+
React.useEffect(() => {
|
|
26204
|
+
if (!bisectCandidateSha) {
|
|
26205
|
+
setBisectCandidateDetail(undefined);
|
|
26206
|
+
setBisectCandidateLoading(false);
|
|
26207
|
+
return;
|
|
26208
|
+
}
|
|
26209
|
+
let active = true;
|
|
26210
|
+
setBisectCandidateLoading(true);
|
|
26211
|
+
void (async () => {
|
|
26212
|
+
const next = await safe(getCommitDetail(git, bisectCandidateSha));
|
|
26213
|
+
if (active) {
|
|
26214
|
+
setBisectCandidateDetail(next);
|
|
26215
|
+
setBisectCandidateLoading(false);
|
|
26216
|
+
}
|
|
26217
|
+
})();
|
|
26218
|
+
return () => { active = false; };
|
|
26219
|
+
}, [git, bisectCandidateSha]);
|
|
26191
26220
|
// #779 — load `git diff <base>..<head>` once the diff view becomes
|
|
26192
26221
|
// active with diffSource='compare'. Mirrors the stash loader's
|
|
26193
26222
|
// shape; the surface renders the lines via the same +/-/@@ coloring
|
|
@@ -27591,7 +27620,7 @@ function LogInkApp(deps) {
|
|
|
27591
27620
|
if (showOnboarding) {
|
|
27592
27621
|
return renderOnboardingOverlay(h, { Box, Text }, layout.rows, layout.columns, theme, appLabel);
|
|
27593
27622
|
}
|
|
27594
|
-
return h(Box, { flexDirection: 'column', height: layout.rows }, renderHeader(h, { Box, Text }, state, context, contextStatus, layout.columns, theme, appLabel), h(Box, { flexDirection: 'row', height: layout.bodyRows }, renderSidebar(h, { Box, Text }, state, context, contextStatus, layout.sidebarWidth, layout.bodyRows, theme), renderMainPanel(h, { Box, Text }, state, context, contextStatus, worktreeDiff, worktreeDiffLoading, worktreeHunks, worktreeHunksLoading, filePreview, filePreviewLoading, commitDiffHunkOffsets, selectedDetailFile, stashDiffLines, stashDiffLoading, compareDiffLines, compareDiffLoading, layout.bodyRows, layout.mainPanelWidth, theme, hasMoreCommits, loadingMoreCommits), renderDetailPanel(h, { Box, Text }, state, context, contextStatus, detail, detailLoading, filePreview, filePreviewLoading, layout.detailWidth, layout.inspectorTabbed, theme)), renderFooter(h, { Box, Text }, state, context, theme, idleTip));
|
|
27623
|
+
return h(Box, { flexDirection: 'column', height: layout.rows }, renderHeader(h, { Box, Text }, state, context, contextStatus, layout.columns, theme, appLabel), h(Box, { flexDirection: 'row', height: layout.bodyRows }, renderSidebar(h, { Box, Text }, state, context, contextStatus, layout.sidebarWidth, layout.bodyRows, theme), renderMainPanel(h, { Box, Text }, state, context, contextStatus, worktreeDiff, worktreeDiffLoading, worktreeHunks, worktreeHunksLoading, filePreview, filePreviewLoading, commitDiffHunkOffsets, selectedDetailFile, stashDiffLines, stashDiffLoading, compareDiffLines, compareDiffLoading, bisectCandidateDetail, bisectCandidateLoading, layout.bodyRows, layout.mainPanelWidth, theme, hasMoreCommits, loadingMoreCommits), renderDetailPanel(h, { Box, Text }, state, context, contextStatus, detail, detailLoading, filePreview, filePreviewLoading, layout.detailWidth, layout.inspectorTabbed, theme)), renderFooter(h, { Box, Text }, state, context, theme, idleTip));
|
|
27595
27624
|
}
|
|
27596
27625
|
function renderHeader(h, components, state, context, contextStatus, columns, theme, appLabel) {
|
|
27597
27626
|
const { Box, Text } = components;
|
|
@@ -27854,7 +27883,7 @@ function renderActiveStatusTabContent(h, Text, context, contextStatus, width, th
|
|
|
27854
27883
|
: []),
|
|
27855
27884
|
];
|
|
27856
27885
|
}
|
|
27857
|
-
function renderMainPanel(h, components, state, context, contextStatus, worktreeDiff, worktreeDiffLoading, worktreeHunks, worktreeHunksLoading, filePreview, filePreviewLoading, commitDiffHunkOffsets, selectedDetailFile, stashDiffLines, stashDiffLoading, compareDiffLines, compareDiffLoading, bodyRows, width, theme, hasMoreCommits, loadingMoreCommits) {
|
|
27886
|
+
function renderMainPanel(h, components, state, context, contextStatus, worktreeDiff, worktreeDiffLoading, worktreeHunks, worktreeHunksLoading, filePreview, filePreviewLoading, commitDiffHunkOffsets, selectedDetailFile, stashDiffLines, stashDiffLoading, compareDiffLines, compareDiffLoading, bisectCandidateDetail, bisectCandidateLoading, bodyRows, width, theme, hasMoreCommits, loadingMoreCommits) {
|
|
27858
27887
|
if (state.activeView === 'status') {
|
|
27859
27888
|
return renderStatusSurface(h, components, state, context, contextStatus, bodyRows, width, theme);
|
|
27860
27889
|
}
|
|
@@ -27874,7 +27903,7 @@ function renderMainPanel(h, components, state, context, contextStatus, worktreeD
|
|
|
27874
27903
|
return renderReflogSurface(h, components, state, context, contextStatus, bodyRows, width, theme);
|
|
27875
27904
|
}
|
|
27876
27905
|
if (state.activeView === 'bisect') {
|
|
27877
|
-
return renderBisectSurface(h, components, state, context, contextStatus, bodyRows, width, theme);
|
|
27906
|
+
return renderBisectSurface(h, components, state, context, contextStatus, bisectCandidateDetail, bisectCandidateLoading, bodyRows, width, theme);
|
|
27878
27907
|
}
|
|
27879
27908
|
if (state.activeView === 'stash') {
|
|
27880
27909
|
return renderStashSurface(h, components, state, context, contextStatus, bodyRows, width, theme);
|
|
@@ -28587,7 +28616,7 @@ function renderReflogSurface(h, components, state, context, contextStatus, bodyR
|
|
|
28587
28616
|
* navigable so the user can read the documentation before starting
|
|
28588
28617
|
* — they can't break anything from here.
|
|
28589
28618
|
*/
|
|
28590
|
-
function renderBisectSurface(h, components, state, context, contextStatus, bodyRows, width, theme) {
|
|
28619
|
+
function renderBisectSurface(h, components, state, context, contextStatus, candidateDetail, candidateLoading, bodyRows, width, theme) {
|
|
28591
28620
|
const { Box, Text } = components;
|
|
28592
28621
|
const focused = state.focus === 'commits';
|
|
28593
28622
|
const loading = isLogInkContextKeyLoading(contextStatus, 'bisect');
|
|
@@ -28598,23 +28627,84 @@ function renderBisectSurface(h, components, state, context, contextStatus, bodyR
|
|
|
28598
28627
|
lines.push(h(Text, { key: 'bisect-loading', dimColor: true }, truncate$1('· Loading bisect status…', width - 4)));
|
|
28599
28628
|
}
|
|
28600
28629
|
else if (!bisect?.active) {
|
|
28601
|
-
//
|
|
28602
|
-
//
|
|
28603
|
-
//
|
|
28604
|
-
//
|
|
28605
|
-
//
|
|
28606
|
-
|
|
28607
|
-
|
|
28608
|
-
|
|
28609
|
-
|
|
28610
|
-
|
|
28611
|
-
|
|
28630
|
+
// Empty-state explainer (#879). Teaches the bisect workflow in
|
|
28631
|
+
// ~30 seconds: what it is, how it works, how to start one (CLI
|
|
28632
|
+
// entry remains the supported on-ramp until the in-TUI start
|
|
28633
|
+
// child item lands), and a tip about picking the good anchor.
|
|
28634
|
+
// Bisect is a rarely-used feature even for experienced users —
|
|
28635
|
+
// shipping it with terse copy assumes muscle memory the median
|
|
28636
|
+
// user doesn't have.
|
|
28637
|
+
const empty = [
|
|
28638
|
+
{ key: 'title', text: 'Bisect — find the commit that introduced a bug.', opts: { bold: true } },
|
|
28639
|
+
{ key: 'spacer-1', text: '' },
|
|
28640
|
+
{ key: 'how-h', text: 'How it works', opts: { bold: true } },
|
|
28641
|
+
{ key: 'how-1', text: ' Binary search through history. You mark commits as "good" (bug' },
|
|
28642
|
+
{ key: 'how-2', text: ' not present) or "bad" (bug present); git narrows the range until' },
|
|
28643
|
+
{ key: 'how-3', text: ' it identifies the first bad commit.' },
|
|
28644
|
+
{ key: 'spacer-2', text: '' },
|
|
28645
|
+
{ key: 'start-h', text: 'How to start', opts: { bold: true } },
|
|
28646
|
+
{ key: 'start-1', text: ' From your shell:' },
|
|
28647
|
+
{ key: 'start-2', text: ' git bisect start <bad-ref> <good-ref>', opts: { accent: true } },
|
|
28648
|
+
{ key: 'start-3', text: ' Then come back here — coco picks up the active bisect and gives' },
|
|
28649
|
+
{ key: 'start-4', text: ' you single-keystroke controls:' },
|
|
28650
|
+
{ key: 'start-5', text: ' g mark good s skip (e.g. doesn\'t build)', opts: { accent: true } },
|
|
28651
|
+
{ key: 'start-6', text: ' b mark bad x reset / cancel', opts: { accent: true } },
|
|
28652
|
+
{ key: 'spacer-3', text: '' },
|
|
28653
|
+
{ key: 'tip-h', text: 'Tip', opts: { bold: true } },
|
|
28654
|
+
{ key: 'tip-1', text: ' Pick a recent release tag as your "good" anchor if you don\'t' },
|
|
28655
|
+
{ key: 'tip-2', text: ' remember when the bug appeared. Tags are visible from the tags' },
|
|
28656
|
+
{ key: 'tip-3', text: ' view (g t).' },
|
|
28657
|
+
];
|
|
28658
|
+
for (const row of empty) {
|
|
28659
|
+
lines.push(h(Text, {
|
|
28660
|
+
key: `bisect-empty-${row.key}`,
|
|
28661
|
+
bold: row.opts?.bold,
|
|
28662
|
+
dimColor: row.opts?.dim,
|
|
28663
|
+
color: row.opts?.accent ? accent : undefined,
|
|
28664
|
+
}, truncate$1(row.text, width - 4)));
|
|
28665
|
+
}
|
|
28612
28666
|
}
|
|
28613
28667
|
else {
|
|
28614
|
-
// Active bisect.
|
|
28615
|
-
//
|
|
28668
|
+
// Active bisect. Three-section body: current candidate (sha +
|
|
28669
|
+
// commit summary so the user can judge the diff at a glance),
|
|
28670
|
+
// recent decisions, action hints. Action keys live in the footer.
|
|
28616
28671
|
const headerSha = bisect.currentSha ? bisect.currentSha.slice(0, 8) : '<unknown>';
|
|
28617
28672
|
lines.push(h(Text, { key: 'bisect-active-title', bold: true }, truncate$1(`Bisecting · current candidate ${headerSha}`, width - 4)));
|
|
28673
|
+
// #879 (item 2) — render commit detail for the current candidate.
|
|
28674
|
+
// Lets the user judge "does this look like it would cause the bug?"
|
|
28675
|
+
// before they run their tests, instead of dropping to shell to
|
|
28676
|
+
// git show. Loading is brief (one git show invocation) and the
|
|
28677
|
+
// surface falls back to just the sha header when the detail
|
|
28678
|
+
// hasn't arrived yet (or git rejected the lookup).
|
|
28679
|
+
if (candidateLoading) {
|
|
28680
|
+
lines.push(h(Text, { key: 'bisect-candidate-loading', dimColor: true }, truncate$1(' loading commit detail…', width - 4)));
|
|
28681
|
+
}
|
|
28682
|
+
else if (candidateDetail) {
|
|
28683
|
+
const { author, date, message, body, stats, files } = candidateDetail;
|
|
28684
|
+
lines.push(h(Text, { key: 'bisect-candidate-subject' }, truncate$1(` ${message}`, width - 4)));
|
|
28685
|
+
lines.push(h(Text, { key: 'bisect-candidate-author', dimColor: true }, truncate$1(` ${author} · ${date}`, width - 4)));
|
|
28686
|
+
// Body line — first non-empty line of the commit body, truncated.
|
|
28687
|
+
// Skip the noisy preamble (subject + blank line) by taking the
|
|
28688
|
+
// first paragraph after the title; body===subject is common for
|
|
28689
|
+
// single-line commits and we filter that out.
|
|
28690
|
+
const firstBodyLine = (body || '')
|
|
28691
|
+
.split('\n')
|
|
28692
|
+
.map((line) => line.trim())
|
|
28693
|
+
.find((line) => line.length > 0 && line !== message);
|
|
28694
|
+
if (firstBodyLine) {
|
|
28695
|
+
lines.push(h(Text, { key: 'bisect-candidate-body', dimColor: true }, truncate$1(` ${firstBodyLine}`, width - 4)));
|
|
28696
|
+
}
|
|
28697
|
+
// Stats summary: total file count + +/- numbers, then a few
|
|
28698
|
+
// file names so the user sees scope at a glance. Cap the
|
|
28699
|
+
// file-name list at 3 entries to keep the section bounded.
|
|
28700
|
+
lines.push(h(Text, { key: 'bisect-candidate-stats' }, truncate$1(` ${stats.filesChanged} file${stats.filesChanged === 1 ? '' : 's'} · +${stats.insertions} / -${stats.deletions}`, width - 4)));
|
|
28701
|
+
const sampleFiles = files.slice(0, 3).map((file) => file.path);
|
|
28702
|
+
if (sampleFiles.length > 0) {
|
|
28703
|
+
const overflow = files.length > sampleFiles.length ? ` (+${files.length - sampleFiles.length} more)` : '';
|
|
28704
|
+
lines.push(h(Text, { key: 'bisect-candidate-files', dimColor: true }, truncate$1(` ${sampleFiles.join(', ')}${overflow}`, width - 4)));
|
|
28705
|
+
}
|
|
28706
|
+
}
|
|
28707
|
+
// Spacer separates the candidate section from decisions.
|
|
28618
28708
|
lines.push(h(Text, { key: 'bisect-active-spacer' }, ''));
|
|
28619
28709
|
const decisions = bisect.log.filter((entry) => entry.kind === 'good' || entry.kind === 'bad' || entry.kind === 'skip');
|
|
28620
28710
|
if (decisions.length === 0) {
|
package/dist/index.js
CHANGED
|
@@ -78,7 +78,7 @@ var readline__namespace = /*#__PURE__*/_interopNamespaceDefault(readline);
|
|
|
78
78
|
/**
|
|
79
79
|
* Current build version from package.json
|
|
80
80
|
*/
|
|
81
|
-
const BUILD_VERSION = "0.
|
|
81
|
+
const BUILD_VERSION = "0.47.0";
|
|
82
82
|
|
|
83
83
|
const isInteractive = (config) => {
|
|
84
84
|
return config?.mode === 'interactive' || !!config?.interactive;
|
|
@@ -26212,6 +26212,35 @@ function LogInkApp(deps) {
|
|
|
26212
26212
|
})();
|
|
26213
26213
|
return () => { active = false; };
|
|
26214
26214
|
}, [git, state.activeView, state.diffSource, state.stashDiffRef]);
|
|
26215
|
+
// #879 (item 2) — load commit detail for the active bisect's
|
|
26216
|
+
// current candidate so the bisect surface can show "what changed
|
|
26217
|
+
// here" alongside the decision keys. Mirrors the history-detail
|
|
26218
|
+
// loader's shape but keyed on `bisect.currentSha` and only fires
|
|
26219
|
+
// when the bisect view is active. Best-effort: any failure leaves
|
|
26220
|
+
// the surface in its non-detail mode (decision log only) — never
|
|
26221
|
+
// crash the workstation because git couldn't resolve a sha.
|
|
26222
|
+
const [bisectCandidateDetail, setBisectCandidateDetail] = React.useState(undefined);
|
|
26223
|
+
const [bisectCandidateLoading, setBisectCandidateLoading] = React.useState(false);
|
|
26224
|
+
const bisectCandidateSha = state.activeView === 'bisect' && context.bisect?.active
|
|
26225
|
+
? context.bisect.currentSha
|
|
26226
|
+
: '';
|
|
26227
|
+
React.useEffect(() => {
|
|
26228
|
+
if (!bisectCandidateSha) {
|
|
26229
|
+
setBisectCandidateDetail(undefined);
|
|
26230
|
+
setBisectCandidateLoading(false);
|
|
26231
|
+
return;
|
|
26232
|
+
}
|
|
26233
|
+
let active = true;
|
|
26234
|
+
setBisectCandidateLoading(true);
|
|
26235
|
+
void (async () => {
|
|
26236
|
+
const next = await safe(getCommitDetail(git, bisectCandidateSha));
|
|
26237
|
+
if (active) {
|
|
26238
|
+
setBisectCandidateDetail(next);
|
|
26239
|
+
setBisectCandidateLoading(false);
|
|
26240
|
+
}
|
|
26241
|
+
})();
|
|
26242
|
+
return () => { active = false; };
|
|
26243
|
+
}, [git, bisectCandidateSha]);
|
|
26215
26244
|
// #779 — load `git diff <base>..<head>` once the diff view becomes
|
|
26216
26245
|
// active with diffSource='compare'. Mirrors the stash loader's
|
|
26217
26246
|
// shape; the surface renders the lines via the same +/-/@@ coloring
|
|
@@ -27615,7 +27644,7 @@ function LogInkApp(deps) {
|
|
|
27615
27644
|
if (showOnboarding) {
|
|
27616
27645
|
return renderOnboardingOverlay(h, { Box, Text }, layout.rows, layout.columns, theme, appLabel);
|
|
27617
27646
|
}
|
|
27618
|
-
return h(Box, { flexDirection: 'column', height: layout.rows }, renderHeader(h, { Box, Text }, state, context, contextStatus, layout.columns, theme, appLabel), h(Box, { flexDirection: 'row', height: layout.bodyRows }, renderSidebar(h, { Box, Text }, state, context, contextStatus, layout.sidebarWidth, layout.bodyRows, theme), renderMainPanel(h, { Box, Text }, state, context, contextStatus, worktreeDiff, worktreeDiffLoading, worktreeHunks, worktreeHunksLoading, filePreview, filePreviewLoading, commitDiffHunkOffsets, selectedDetailFile, stashDiffLines, stashDiffLoading, compareDiffLines, compareDiffLoading, layout.bodyRows, layout.mainPanelWidth, theme, hasMoreCommits, loadingMoreCommits), renderDetailPanel(h, { Box, Text }, state, context, contextStatus, detail, detailLoading, filePreview, filePreviewLoading, layout.detailWidth, layout.inspectorTabbed, theme)), renderFooter(h, { Box, Text }, state, context, theme, idleTip));
|
|
27647
|
+
return h(Box, { flexDirection: 'column', height: layout.rows }, renderHeader(h, { Box, Text }, state, context, contextStatus, layout.columns, theme, appLabel), h(Box, { flexDirection: 'row', height: layout.bodyRows }, renderSidebar(h, { Box, Text }, state, context, contextStatus, layout.sidebarWidth, layout.bodyRows, theme), renderMainPanel(h, { Box, Text }, state, context, contextStatus, worktreeDiff, worktreeDiffLoading, worktreeHunks, worktreeHunksLoading, filePreview, filePreviewLoading, commitDiffHunkOffsets, selectedDetailFile, stashDiffLines, stashDiffLoading, compareDiffLines, compareDiffLoading, bisectCandidateDetail, bisectCandidateLoading, layout.bodyRows, layout.mainPanelWidth, theme, hasMoreCommits, loadingMoreCommits), renderDetailPanel(h, { Box, Text }, state, context, contextStatus, detail, detailLoading, filePreview, filePreviewLoading, layout.detailWidth, layout.inspectorTabbed, theme)), renderFooter(h, { Box, Text }, state, context, theme, idleTip));
|
|
27619
27648
|
}
|
|
27620
27649
|
function renderHeader(h, components, state, context, contextStatus, columns, theme, appLabel) {
|
|
27621
27650
|
const { Box, Text } = components;
|
|
@@ -27878,7 +27907,7 @@ function renderActiveStatusTabContent(h, Text, context, contextStatus, width, th
|
|
|
27878
27907
|
: []),
|
|
27879
27908
|
];
|
|
27880
27909
|
}
|
|
27881
|
-
function renderMainPanel(h, components, state, context, contextStatus, worktreeDiff, worktreeDiffLoading, worktreeHunks, worktreeHunksLoading, filePreview, filePreviewLoading, commitDiffHunkOffsets, selectedDetailFile, stashDiffLines, stashDiffLoading, compareDiffLines, compareDiffLoading, bodyRows, width, theme, hasMoreCommits, loadingMoreCommits) {
|
|
27910
|
+
function renderMainPanel(h, components, state, context, contextStatus, worktreeDiff, worktreeDiffLoading, worktreeHunks, worktreeHunksLoading, filePreview, filePreviewLoading, commitDiffHunkOffsets, selectedDetailFile, stashDiffLines, stashDiffLoading, compareDiffLines, compareDiffLoading, bisectCandidateDetail, bisectCandidateLoading, bodyRows, width, theme, hasMoreCommits, loadingMoreCommits) {
|
|
27882
27911
|
if (state.activeView === 'status') {
|
|
27883
27912
|
return renderStatusSurface(h, components, state, context, contextStatus, bodyRows, width, theme);
|
|
27884
27913
|
}
|
|
@@ -27898,7 +27927,7 @@ function renderMainPanel(h, components, state, context, contextStatus, worktreeD
|
|
|
27898
27927
|
return renderReflogSurface(h, components, state, context, contextStatus, bodyRows, width, theme);
|
|
27899
27928
|
}
|
|
27900
27929
|
if (state.activeView === 'bisect') {
|
|
27901
|
-
return renderBisectSurface(h, components, state, context, contextStatus, bodyRows, width, theme);
|
|
27930
|
+
return renderBisectSurface(h, components, state, context, contextStatus, bisectCandidateDetail, bisectCandidateLoading, bodyRows, width, theme);
|
|
27902
27931
|
}
|
|
27903
27932
|
if (state.activeView === 'stash') {
|
|
27904
27933
|
return renderStashSurface(h, components, state, context, contextStatus, bodyRows, width, theme);
|
|
@@ -28611,7 +28640,7 @@ function renderReflogSurface(h, components, state, context, contextStatus, bodyR
|
|
|
28611
28640
|
* navigable so the user can read the documentation before starting
|
|
28612
28641
|
* — they can't break anything from here.
|
|
28613
28642
|
*/
|
|
28614
|
-
function renderBisectSurface(h, components, state, context, contextStatus, bodyRows, width, theme) {
|
|
28643
|
+
function renderBisectSurface(h, components, state, context, contextStatus, candidateDetail, candidateLoading, bodyRows, width, theme) {
|
|
28615
28644
|
const { Box, Text } = components;
|
|
28616
28645
|
const focused = state.focus === 'commits';
|
|
28617
28646
|
const loading = isLogInkContextKeyLoading(contextStatus, 'bisect');
|
|
@@ -28622,23 +28651,84 @@ function renderBisectSurface(h, components, state, context, contextStatus, bodyR
|
|
|
28622
28651
|
lines.push(h(Text, { key: 'bisect-loading', dimColor: true }, truncate$1('· Loading bisect status…', width - 4)));
|
|
28623
28652
|
}
|
|
28624
28653
|
else if (!bisect?.active) {
|
|
28625
|
-
//
|
|
28626
|
-
//
|
|
28627
|
-
//
|
|
28628
|
-
//
|
|
28629
|
-
//
|
|
28630
|
-
|
|
28631
|
-
|
|
28632
|
-
|
|
28633
|
-
|
|
28634
|
-
|
|
28635
|
-
|
|
28654
|
+
// Empty-state explainer (#879). Teaches the bisect workflow in
|
|
28655
|
+
// ~30 seconds: what it is, how it works, how to start one (CLI
|
|
28656
|
+
// entry remains the supported on-ramp until the in-TUI start
|
|
28657
|
+
// child item lands), and a tip about picking the good anchor.
|
|
28658
|
+
// Bisect is a rarely-used feature even for experienced users —
|
|
28659
|
+
// shipping it with terse copy assumes muscle memory the median
|
|
28660
|
+
// user doesn't have.
|
|
28661
|
+
const empty = [
|
|
28662
|
+
{ key: 'title', text: 'Bisect — find the commit that introduced a bug.', opts: { bold: true } },
|
|
28663
|
+
{ key: 'spacer-1', text: '' },
|
|
28664
|
+
{ key: 'how-h', text: 'How it works', opts: { bold: true } },
|
|
28665
|
+
{ key: 'how-1', text: ' Binary search through history. You mark commits as "good" (bug' },
|
|
28666
|
+
{ key: 'how-2', text: ' not present) or "bad" (bug present); git narrows the range until' },
|
|
28667
|
+
{ key: 'how-3', text: ' it identifies the first bad commit.' },
|
|
28668
|
+
{ key: 'spacer-2', text: '' },
|
|
28669
|
+
{ key: 'start-h', text: 'How to start', opts: { bold: true } },
|
|
28670
|
+
{ key: 'start-1', text: ' From your shell:' },
|
|
28671
|
+
{ key: 'start-2', text: ' git bisect start <bad-ref> <good-ref>', opts: { accent: true } },
|
|
28672
|
+
{ key: 'start-3', text: ' Then come back here — coco picks up the active bisect and gives' },
|
|
28673
|
+
{ key: 'start-4', text: ' you single-keystroke controls:' },
|
|
28674
|
+
{ key: 'start-5', text: ' g mark good s skip (e.g. doesn\'t build)', opts: { accent: true } },
|
|
28675
|
+
{ key: 'start-6', text: ' b mark bad x reset / cancel', opts: { accent: true } },
|
|
28676
|
+
{ key: 'spacer-3', text: '' },
|
|
28677
|
+
{ key: 'tip-h', text: 'Tip', opts: { bold: true } },
|
|
28678
|
+
{ key: 'tip-1', text: ' Pick a recent release tag as your "good" anchor if you don\'t' },
|
|
28679
|
+
{ key: 'tip-2', text: ' remember when the bug appeared. Tags are visible from the tags' },
|
|
28680
|
+
{ key: 'tip-3', text: ' view (g t).' },
|
|
28681
|
+
];
|
|
28682
|
+
for (const row of empty) {
|
|
28683
|
+
lines.push(h(Text, {
|
|
28684
|
+
key: `bisect-empty-${row.key}`,
|
|
28685
|
+
bold: row.opts?.bold,
|
|
28686
|
+
dimColor: row.opts?.dim,
|
|
28687
|
+
color: row.opts?.accent ? accent : undefined,
|
|
28688
|
+
}, truncate$1(row.text, width - 4)));
|
|
28689
|
+
}
|
|
28636
28690
|
}
|
|
28637
28691
|
else {
|
|
28638
|
-
// Active bisect.
|
|
28639
|
-
//
|
|
28692
|
+
// Active bisect. Three-section body: current candidate (sha +
|
|
28693
|
+
// commit summary so the user can judge the diff at a glance),
|
|
28694
|
+
// recent decisions, action hints. Action keys live in the footer.
|
|
28640
28695
|
const headerSha = bisect.currentSha ? bisect.currentSha.slice(0, 8) : '<unknown>';
|
|
28641
28696
|
lines.push(h(Text, { key: 'bisect-active-title', bold: true }, truncate$1(`Bisecting · current candidate ${headerSha}`, width - 4)));
|
|
28697
|
+
// #879 (item 2) — render commit detail for the current candidate.
|
|
28698
|
+
// Lets the user judge "does this look like it would cause the bug?"
|
|
28699
|
+
// before they run their tests, instead of dropping to shell to
|
|
28700
|
+
// git show. Loading is brief (one git show invocation) and the
|
|
28701
|
+
// surface falls back to just the sha header when the detail
|
|
28702
|
+
// hasn't arrived yet (or git rejected the lookup).
|
|
28703
|
+
if (candidateLoading) {
|
|
28704
|
+
lines.push(h(Text, { key: 'bisect-candidate-loading', dimColor: true }, truncate$1(' loading commit detail…', width - 4)));
|
|
28705
|
+
}
|
|
28706
|
+
else if (candidateDetail) {
|
|
28707
|
+
const { author, date, message, body, stats, files } = candidateDetail;
|
|
28708
|
+
lines.push(h(Text, { key: 'bisect-candidate-subject' }, truncate$1(` ${message}`, width - 4)));
|
|
28709
|
+
lines.push(h(Text, { key: 'bisect-candidate-author', dimColor: true }, truncate$1(` ${author} · ${date}`, width - 4)));
|
|
28710
|
+
// Body line — first non-empty line of the commit body, truncated.
|
|
28711
|
+
// Skip the noisy preamble (subject + blank line) by taking the
|
|
28712
|
+
// first paragraph after the title; body===subject is common for
|
|
28713
|
+
// single-line commits and we filter that out.
|
|
28714
|
+
const firstBodyLine = (body || '')
|
|
28715
|
+
.split('\n')
|
|
28716
|
+
.map((line) => line.trim())
|
|
28717
|
+
.find((line) => line.length > 0 && line !== message);
|
|
28718
|
+
if (firstBodyLine) {
|
|
28719
|
+
lines.push(h(Text, { key: 'bisect-candidate-body', dimColor: true }, truncate$1(` ${firstBodyLine}`, width - 4)));
|
|
28720
|
+
}
|
|
28721
|
+
// Stats summary: total file count + +/- numbers, then a few
|
|
28722
|
+
// file names so the user sees scope at a glance. Cap the
|
|
28723
|
+
// file-name list at 3 entries to keep the section bounded.
|
|
28724
|
+
lines.push(h(Text, { key: 'bisect-candidate-stats' }, truncate$1(` ${stats.filesChanged} file${stats.filesChanged === 1 ? '' : 's'} · +${stats.insertions} / -${stats.deletions}`, width - 4)));
|
|
28725
|
+
const sampleFiles = files.slice(0, 3).map((file) => file.path);
|
|
28726
|
+
if (sampleFiles.length > 0) {
|
|
28727
|
+
const overflow = files.length > sampleFiles.length ? ` (+${files.length - sampleFiles.length} more)` : '';
|
|
28728
|
+
lines.push(h(Text, { key: 'bisect-candidate-files', dimColor: true }, truncate$1(` ${sampleFiles.join(', ')}${overflow}`, width - 4)));
|
|
28729
|
+
}
|
|
28730
|
+
}
|
|
28731
|
+
// Spacer separates the candidate section from decisions.
|
|
28642
28732
|
lines.push(h(Text, { key: 'bisect-active-spacer' }, ''));
|
|
28643
28733
|
const decisions = bisect.log.filter((entry) => entry.kind === 'good' || entry.kind === 'bad' || entry.kind === 'skip');
|
|
28644
28734
|
if (decisions.length === 0) {
|