git-coco 0.62.1 → 0.62.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/dist/index.esm.mjs +109 -47
- package/dist/index.js +109 -47
- package/package.json +1 -1
package/dist/index.esm.mjs
CHANGED
|
@@ -61,7 +61,7 @@ import { pathToFileURL } from 'url';
|
|
|
61
61
|
/**
|
|
62
62
|
* Current build version from package.json
|
|
63
63
|
*/
|
|
64
|
-
const BUILD_VERSION = "0.62.
|
|
64
|
+
const BUILD_VERSION = "0.62.2";
|
|
65
65
|
|
|
66
66
|
const isInteractive = (config) => {
|
|
67
67
|
return config?.mode === 'interactive' || !!config?.interactive;
|
|
@@ -25162,12 +25162,14 @@ function formatSortIndicator(mode, options = {}) {
|
|
|
25162
25162
|
}
|
|
25163
25163
|
|
|
25164
25164
|
/**
|
|
25165
|
-
* True when `pending` (a `state.
|
|
25166
|
-
*
|
|
25167
|
-
*
|
|
25168
|
-
*
|
|
25165
|
+
* True when `pending` (a `state.pendingItemAction`) targets this exact
|
|
25166
|
+
* row. Action-agnostic on purpose — every surface + the sidebar render
|
|
25167
|
+
* the same spinner whether the row is being deleted or checked out, so
|
|
25168
|
+
* the spinner-swap test stays identical everywhere. Takes the field
|
|
25169
|
+
* value (not the whole state) so it can live next to the type without a
|
|
25170
|
+
* forward reference.
|
|
25169
25171
|
*/
|
|
25170
|
-
function
|
|
25172
|
+
function isPendingItemAction(pending, kind, id) {
|
|
25171
25173
|
return pending?.kind === kind && pending.id === id;
|
|
25172
25174
|
}
|
|
25173
25175
|
const DEFAULT_CHANGELOG_VIEW_STATE = {
|
|
@@ -26375,10 +26377,11 @@ function applyLogInkAction(state, action) {
|
|
|
26375
26377
|
workflowActionId: action.value ? undefined : state.workflowActionId,
|
|
26376
26378
|
pendingKey: undefined,
|
|
26377
26379
|
};
|
|
26378
|
-
case '
|
|
26379
|
-
// Pure marker for the in-flight
|
|
26380
|
-
//
|
|
26381
|
-
|
|
26380
|
+
case 'setPendingItemAction':
|
|
26381
|
+
// Pure marker for the in-flight row action (delete / checkout);
|
|
26382
|
+
// touches nothing else so the list keeps rendering normally
|
|
26383
|
+
// underneath the one spinner'd row.
|
|
26384
|
+
return { ...state, pendingItemAction: action.value };
|
|
26382
26385
|
case 'toggleFilterMode':
|
|
26383
26386
|
return {
|
|
26384
26387
|
...state,
|
|
@@ -31001,6 +31004,29 @@ function deleteBranch(git, branch, force = false) {
|
|
|
31001
31004
|
function isBranchNotFullyMergedError(message) {
|
|
31002
31005
|
return /not fully merged/i.test(message || '');
|
|
31003
31006
|
}
|
|
31007
|
+
/**
|
|
31008
|
+
* True when a branch delete was rejected because the branch is checked
|
|
31009
|
+
* out in a worktree. Unlike "not fully merged" there's no force escape
|
|
31010
|
+
* hatch — git refuses `git branch -D` on a worktree-checked-out branch
|
|
31011
|
+
* too — so the UI should surface a clear "free up the worktree first"
|
|
31012
|
+
* message rather than offering a force-delete that would fail the same
|
|
31013
|
+
* way. Matches git's wording: `Cannot delete branch 'x' checked out at
|
|
31014
|
+
* '<path>'` / `used by worktree at '<path>'`.
|
|
31015
|
+
*/
|
|
31016
|
+
function isBranchCheckedOutElsewhereError(message) {
|
|
31017
|
+
return /checked out at|used by worktree/i.test(message || '');
|
|
31018
|
+
}
|
|
31019
|
+
/**
|
|
31020
|
+
* Pull the worktree path out of git's "checked out at '<path>'" /
|
|
31021
|
+
* "used by worktree at '<path>'" rejection so the UI can name where the
|
|
31022
|
+
* branch is still in use. Returns undefined when the message doesn't
|
|
31023
|
+
* carry a path (older git phrasings) so callers can fall back to a
|
|
31024
|
+
* generic message.
|
|
31025
|
+
*/
|
|
31026
|
+
function parseCheckedOutWorktreePath(message) {
|
|
31027
|
+
const match = /(?:checked out at|used by worktree at) '([^']+)'/i.exec(message || '');
|
|
31028
|
+
return match?.[1];
|
|
31029
|
+
}
|
|
31004
31030
|
function fetchRemotes(git) {
|
|
31005
31031
|
return runAction$5(() => git.raw(['fetch', '--all', '--prune']), 'Fetched all remotes');
|
|
31006
31032
|
}
|
|
@@ -34412,7 +34438,7 @@ function renderActiveSidebarContent(h, Text, tab, state, context, contextStatus,
|
|
|
34412
34438
|
// shows this spinner in place of its leading marker (branches /
|
|
34413
34439
|
// worktrees) or appended to the row (tags / stashes, which have no
|
|
34414
34440
|
// leading status icon). `pending` is the single in-flight target.
|
|
34415
|
-
const pending = state.
|
|
34441
|
+
const pending = state.pendingItemAction;
|
|
34416
34442
|
const spin = inlineSpinnerGlyph(spinnerFrame, theme.ascii);
|
|
34417
34443
|
// Available rows for the active tab's list. The sidebar chrome
|
|
34418
34444
|
// takes ~10 rows (panel title + spacer + 5 tab headers + 4 inter-tab
|
|
@@ -34451,7 +34477,7 @@ function renderActiveSidebarContent(h, Text, tab, state, context, contextStatus,
|
|
|
34451
34477
|
return [
|
|
34452
34478
|
...headerRows,
|
|
34453
34479
|
...renderSelectableSidebarRows(h, Text, sortedBranches, state.selectedBranchIndex, focused, width, theme, (branch) => {
|
|
34454
|
-
const glyph =
|
|
34480
|
+
const glyph = isPendingItemAction(pending, 'branch', branch.shortName)
|
|
34455
34481
|
? spin
|
|
34456
34482
|
: branchRowMarker(branch, { ascii: theme.ascii }).glyph;
|
|
34457
34483
|
return `${glyph} ${branch.shortName}`;
|
|
@@ -34470,7 +34496,7 @@ function renderActiveSidebarContent(h, Text, tab, state, context, contextStatus,
|
|
|
34470
34496
|
const base = `${truncateCells(tag.name, 16)} ${tag.subject}`;
|
|
34471
34497
|
// Tags have no leading status icon, so the pending spinner is
|
|
34472
34498
|
// appended to the row instead of replacing a glyph.
|
|
34473
|
-
return
|
|
34499
|
+
return isPendingItemAction(pending, 'tag', tag.name) ? `${base} ${spin}` : base;
|
|
34474
34500
|
}, 'tab-tags', visibleListCount);
|
|
34475
34501
|
}
|
|
34476
34502
|
if (tab === 'stashes') {
|
|
@@ -34485,7 +34511,7 @@ function renderActiveSidebarContent(h, Text, tab, state, context, contextStatus,
|
|
|
34485
34511
|
const base = `@{${index}} ${stash.message || '(no message)'}`;
|
|
34486
34512
|
// `@{N}` is the stash ref, not a status icon, so append the
|
|
34487
34513
|
// spinner rather than replacing it.
|
|
34488
|
-
return
|
|
34514
|
+
return isPendingItemAction(pending, 'stash', stash.ref) ? `${base} ${spin}` : base;
|
|
34489
34515
|
}, 'tab-stashes', visibleListCount);
|
|
34490
34516
|
}
|
|
34491
34517
|
// worktrees
|
|
@@ -34497,7 +34523,7 @@ function renderActiveSidebarContent(h, Text, tab, state, context, contextStatus,
|
|
|
34497
34523
|
return [h(Text, { key: 'tab-worktrees-empty', dimColor: true }, ' No linked worktrees')];
|
|
34498
34524
|
}
|
|
34499
34525
|
return renderSelectableSidebarRows(h, Text, worktrees, state.selectedWorktreeListIndex, focused, width, theme, (worktree) => {
|
|
34500
|
-
const marker =
|
|
34526
|
+
const marker = isPendingItemAction(pending, 'worktree', worktree.path)
|
|
34501
34527
|
? spin
|
|
34502
34528
|
: worktree.current ? '*' : ' ';
|
|
34503
34529
|
const wstate = worktree.dirty ? 'dirty' : 'clean';
|
|
@@ -34933,7 +34959,7 @@ function renderBranchesSurface(ctx, spinnerFrame = 0) {
|
|
|
34933
34959
|
// While this branch's delete is in flight, its sync-state marker
|
|
34934
34960
|
// is replaced by an inline spinner (accent-coloured) so the row
|
|
34935
34961
|
// reads as "deleting" until it vanishes on refresh.
|
|
34936
|
-
const deleting =
|
|
34962
|
+
const deleting = isPendingItemAction(state.pendingItemAction, 'branch', branch.shortName);
|
|
34937
34963
|
const glyph = deleting ? inlineSpinnerGlyph(spinnerFrame, theme.ascii) : marker.glyph;
|
|
34938
34964
|
const glyphColor = deleting
|
|
34939
34965
|
? (theme.noColor ? undefined : theme.colors.accent)
|
|
@@ -38823,7 +38849,7 @@ function renderStashSurface(ctx, spinnerFrame = 0) {
|
|
|
38823
38849
|
// The `stash@{N}` ref is an identifier, not a status icon, so a
|
|
38824
38850
|
// delete-in-flight appends an accent spinner at the row's end
|
|
38825
38851
|
// (2 cells reserved from the width budget).
|
|
38826
|
-
const deleting =
|
|
38852
|
+
const deleting = isPendingItemAction(state.pendingItemAction, 'stash', stash.ref);
|
|
38827
38853
|
const spinnerSpan = deleting
|
|
38828
38854
|
? h(Text, { color: theme.noColor ? undefined : theme.colors.accent, dimColor: false }, ` ${inlineSpinnerGlyph(spinnerFrame, theme.ascii)}`)
|
|
38829
38855
|
: null;
|
|
@@ -39223,7 +39249,7 @@ function renderTagsSurface(ctx, spinnerFrame = 0) {
|
|
|
39223
39249
|
// Tags have no leading status icon, so a delete-in-flight appends
|
|
39224
39250
|
// an accent spinner at the row's end. Reserve its 2 cells from the
|
|
39225
39251
|
// truncation budget so it never pushes the row past the panel.
|
|
39226
|
-
const deleting =
|
|
39252
|
+
const deleting = isPendingItemAction(state.pendingItemAction, 'tag', tag.name);
|
|
39227
39253
|
const spinnerSpan = deleting
|
|
39228
39254
|
? h(Text, { color: theme.noColor ? undefined : theme.colors.accent, dimColor: false }, ` ${inlineSpinnerGlyph(spinnerFrame, theme.ascii)}`)
|
|
39229
39255
|
: null;
|
|
@@ -39305,7 +39331,7 @@ function renderWorktreesSurface(ctx, spinnerFrame = 0) {
|
|
|
39305
39331
|
const index = startIndex + offset;
|
|
39306
39332
|
const isSelected = index === selected;
|
|
39307
39333
|
const cursor = isSelected ? '>' : ' ';
|
|
39308
|
-
const marker =
|
|
39334
|
+
const marker = isPendingItemAction(state.pendingItemAction, 'worktree', entry.path)
|
|
39309
39335
|
? inlineSpinnerGlyph(spinnerFrame, theme.ascii)
|
|
39310
39336
|
: entry.current ? '*' : ' ';
|
|
39311
39337
|
const branchLabel = entry.branch ? entry.branch : entry.head || '<detached>';
|
|
@@ -40774,15 +40800,28 @@ const REMOTE_OP_LOADERS = {
|
|
|
40774
40800
|
* Returns `undefined` for non-delete workflows (and when nothing is
|
|
40775
40801
|
* selected), which the runner treats as "no pending marker".
|
|
40776
40802
|
*/
|
|
40777
|
-
function
|
|
40803
|
+
function resolvePendingItemAction(id, state, context) {
|
|
40778
40804
|
const { filter } = state;
|
|
40805
|
+
// Checking out a branch gets the same inline spinner on its row as a
|
|
40806
|
+
// delete does — the action just runs `git checkout` instead of
|
|
40807
|
+
// `git branch -d`. Resolved the same way as the delete branch case
|
|
40808
|
+
// (and identically to the checkout-branch handler) so the spinner
|
|
40809
|
+
// lands on exactly the row the user pressed enter on.
|
|
40810
|
+
if (id === 'checkout-branch') {
|
|
40811
|
+
const all = sortBranches(context.branches?.localBranches || [], state.branchSort);
|
|
40812
|
+
const visible = filter
|
|
40813
|
+
? all.filter((b) => matchesPromotedFilter([b.shortName, b.upstream || ''], filter))
|
|
40814
|
+
: all;
|
|
40815
|
+
const branch = visible[Math.min(state.selectedBranchIndex, visible.length - 1)];
|
|
40816
|
+
return branch ? { kind: 'branch', id: branch.shortName, action: 'checkout' } : undefined;
|
|
40817
|
+
}
|
|
40779
40818
|
if (id === 'delete-branch' || id === 'force-delete-branch') {
|
|
40780
40819
|
const all = sortBranches(context.branches?.localBranches || [], state.branchSort);
|
|
40781
40820
|
const visible = filter
|
|
40782
40821
|
? all.filter((b) => matchesPromotedFilter([b.shortName, b.upstream || ''], filter))
|
|
40783
40822
|
: all;
|
|
40784
40823
|
const branch = visible[Math.min(state.selectedBranchIndex, visible.length - 1)];
|
|
40785
|
-
return branch ? { kind: 'branch', id: branch.shortName } : undefined;
|
|
40824
|
+
return branch ? { kind: 'branch', id: branch.shortName, action: 'delete' } : undefined;
|
|
40786
40825
|
}
|
|
40787
40826
|
if (id === 'delete-tag') {
|
|
40788
40827
|
const all = sortTags(context.tags?.tags || [], state.tagSort);
|
|
@@ -40790,7 +40829,7 @@ function resolvePendingDeletion(id, state, context) {
|
|
|
40790
40829
|
? all.filter((t) => matchesPromotedFilter([t.name, t.subject], filter))
|
|
40791
40830
|
: all;
|
|
40792
40831
|
const tag = visible[Math.min(state.selectedTagIndex, visible.length - 1)];
|
|
40793
|
-
return tag ? { kind: 'tag', id: tag.name } : undefined;
|
|
40832
|
+
return tag ? { kind: 'tag', id: tag.name, action: 'delete' } : undefined;
|
|
40794
40833
|
}
|
|
40795
40834
|
if (id === 'drop-stash') {
|
|
40796
40835
|
const all = context.stashes?.stashes || [];
|
|
@@ -40798,7 +40837,7 @@ function resolvePendingDeletion(id, state, context) {
|
|
|
40798
40837
|
? all.filter((s) => matchesPromotedFilter([s.ref, s.message], filter))
|
|
40799
40838
|
: all;
|
|
40800
40839
|
const stash = visible[Math.min(state.selectedStashIndex, visible.length - 1)];
|
|
40801
|
-
return stash ? { kind: 'stash', id: stash.ref } : undefined;
|
|
40840
|
+
return stash ? { kind: 'stash', id: stash.ref, action: 'delete' } : undefined;
|
|
40802
40841
|
}
|
|
40803
40842
|
if (id === 'remove-worktree') {
|
|
40804
40843
|
const all = context.worktreeList?.worktrees || [];
|
|
@@ -40808,7 +40847,7 @@ function resolvePendingDeletion(id, state, context) {
|
|
|
40808
40847
|
const wt = visible.length
|
|
40809
40848
|
? visible[Math.min(state.selectedWorktreeListIndex, visible.length - 1)]
|
|
40810
40849
|
: all[Math.min(state.selectedWorktreeListIndex, Math.max(0, all.length - 1))];
|
|
40811
|
-
return wt ? { kind: 'worktree', id: wt.path } : undefined;
|
|
40850
|
+
return wt ? { kind: 'worktree', id: wt.path, action: 'delete' } : undefined;
|
|
40812
40851
|
}
|
|
40813
40852
|
return undefined;
|
|
40814
40853
|
}
|
|
@@ -41129,9 +41168,10 @@ function LogInkApp(deps) {
|
|
|
41129
41168
|
state.commitCompose.loading ||
|
|
41130
41169
|
Boolean(state.remoteOp) ||
|
|
41131
41170
|
Boolean(state.statusLoading) ||
|
|
41132
|
-
// Keep the shared spinner ticking while a list-item delete
|
|
41133
|
-
// flight so its inline pending glyph animates
|
|
41134
|
-
|
|
41171
|
+
// Keep the shared spinner ticking while a list-item action (delete
|
|
41172
|
+
// or checkout) is in flight so its inline pending glyph animates
|
|
41173
|
+
// instead of freezing.
|
|
41174
|
+
Boolean(state.pendingItemAction);
|
|
41135
41175
|
React.useEffect(() => {
|
|
41136
41176
|
if (!anyLoading) {
|
|
41137
41177
|
// Reset to 0 so the next loading state starts from a known
|
|
@@ -44071,14 +44111,15 @@ function LogInkApp(deps) {
|
|
|
44071
44111
|
if (remoteOp) {
|
|
44072
44112
|
dispatch({ type: 'setRemoteOp', value: remoteOp });
|
|
44073
44113
|
}
|
|
44074
|
-
// Mark the cursored row as
|
|
44075
|
-
// spinner while the git call runs. Cleared in
|
|
44076
|
-
// refresh, so a successful delete hands straight
|
|
44077
|
-
// vanishing,
|
|
44078
|
-
// the
|
|
44079
|
-
|
|
44080
|
-
|
|
44081
|
-
|
|
44114
|
+
// Mark the cursored row as busy so it shows an inline pending
|
|
44115
|
+
// spinner while the git call runs (delete or checkout). Cleared in
|
|
44116
|
+
// `finally` after the refresh, so a successful delete hands straight
|
|
44117
|
+
// off to the row vanishing, a checkout to the sidebar repainting
|
|
44118
|
+
// with the new current branch, and a failure (e.g. an unmerged
|
|
44119
|
+
// branch) restores the row's normal icon alongside the error status.
|
|
44120
|
+
const pendingItemAction = resolvePendingItemAction(id, state, context);
|
|
44121
|
+
if (pendingItemAction) {
|
|
44122
|
+
dispatch({ type: 'setPendingItemAction', value: pendingItemAction });
|
|
44082
44123
|
}
|
|
44083
44124
|
try {
|
|
44084
44125
|
const result = await handler();
|
|
@@ -44091,6 +44132,24 @@ function LogInkApp(deps) {
|
|
|
44091
44132
|
if (id === 'delete-branch' && !result?.ok && isBranchNotFullyMergedError(result?.message)) {
|
|
44092
44133
|
dispatch({ type: 'setPendingConfirmation', value: 'force-delete-branch' });
|
|
44093
44134
|
}
|
|
44135
|
+
// A branch checked out in a worktree can't be deleted — and unlike
|
|
44136
|
+
// the unmerged case, `git branch -D` won't force it either, so we
|
|
44137
|
+
// don't offer a confirmation. Replace git's raw rejection with a
|
|
44138
|
+
// clear "free up the worktree first" message that names where the
|
|
44139
|
+
// branch is still in use.
|
|
44140
|
+
if ((id === 'delete-branch' || id === 'force-delete-branch') &&
|
|
44141
|
+
!result?.ok &&
|
|
44142
|
+
isBranchCheckedOutElsewhereError(result?.message)) {
|
|
44143
|
+
const worktreePath = parseCheckedOutWorktreePath(result?.message);
|
|
44144
|
+
const branchName = pendingItemAction?.id;
|
|
44145
|
+
dispatch({
|
|
44146
|
+
type: 'setStatus',
|
|
44147
|
+
value: worktreePath
|
|
44148
|
+
? `Can't delete ${branchName ? `'${branchName}'` : 'branch'} — checked out in worktree ${worktreePath}. Switch that worktree off the branch or remove it first.`
|
|
44149
|
+
: `Can't delete ${branchName ? `'${branchName}'` : 'branch'} — it's checked out in another worktree. Switch that worktree off the branch or remove it first.`,
|
|
44150
|
+
kind: 'warning',
|
|
44151
|
+
});
|
|
44152
|
+
}
|
|
44094
44153
|
// Refresh history rows AS WELL when the workflow could have
|
|
44095
44154
|
// changed the commits the user sees (#945 follow-up). The
|
|
44096
44155
|
// workflow IDs below all either create/rewrite local commits or
|
|
@@ -44126,15 +44185,18 @@ function LogInkApp(deps) {
|
|
|
44126
44185
|
if (result?.ok && historyMutatingIds.has(id)) {
|
|
44127
44186
|
await refreshHistoryRows();
|
|
44128
44187
|
}
|
|
44129
|
-
// Checkout-branch
|
|
44130
|
-
// refresh
|
|
44131
|
-
//
|
|
44132
|
-
//
|
|
44133
|
-
//
|
|
44134
|
-
// the
|
|
44188
|
+
// Checkout-branch snaps the cursor to position 0 first so when the
|
|
44189
|
+
// refresh completes and the new current branch lands at the top
|
|
44190
|
+
// (per #809's pin-current rule), the cursor is already there
|
|
44191
|
+
// waiting. The refresh is *silent*: the loud refresh used to blank
|
|
44192
|
+
// every branch name behind a "loading branches…" placeholder (#806),
|
|
44193
|
+
// but the in-flight row now carries its own inline pending spinner
|
|
44194
|
+
// (resolvePendingItemAction → action 'checkout'), so a silent
|
|
44195
|
+
// stale-while-revalidate swap keeps the list readable and just
|
|
44196
|
+
// repaints the current-branch marker once the new context lands.
|
|
44135
44197
|
if (id === 'checkout-branch' && result?.ok) {
|
|
44136
44198
|
dispatch({ type: 'resetBranchSelection' });
|
|
44137
|
-
await refreshContext();
|
|
44199
|
+
await refreshContext({ silent: true });
|
|
44138
44200
|
}
|
|
44139
44201
|
else {
|
|
44140
44202
|
// Silent refresh so the deleted item disappears from the list
|
|
@@ -44189,11 +44251,11 @@ function LogInkApp(deps) {
|
|
|
44189
44251
|
if (remoteOp) {
|
|
44190
44252
|
dispatch({ type: 'setRemoteOp', value: undefined });
|
|
44191
44253
|
}
|
|
44192
|
-
// Same guarantee for the per-row
|
|
44193
|
-
// the
|
|
44194
|
-
// left spinning forever.
|
|
44195
|
-
if (
|
|
44196
|
-
dispatch({ type: '
|
|
44254
|
+
// Same guarantee for the per-row pending spinner (delete or
|
|
44255
|
+
// checkout): clear it whether the action succeeded, failed, or the
|
|
44256
|
+
// refresh threw, so no row is left spinning forever.
|
|
44257
|
+
if (pendingItemAction) {
|
|
44258
|
+
dispatch({ type: 'setPendingItemAction', value: undefined });
|
|
44197
44259
|
}
|
|
44198
44260
|
}
|
|
44199
44261
|
}, [context, dispatch, git, refreshContext, refreshHistoryRows, refreshWorktreeContext,
|
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.62.
|
|
81
|
+
const BUILD_VERSION = "0.62.2";
|
|
82
82
|
|
|
83
83
|
const isInteractive = (config) => {
|
|
84
84
|
return config?.mode === 'interactive' || !!config?.interactive;
|
|
@@ -25179,12 +25179,14 @@ function formatSortIndicator(mode, options = {}) {
|
|
|
25179
25179
|
}
|
|
25180
25180
|
|
|
25181
25181
|
/**
|
|
25182
|
-
* True when `pending` (a `state.
|
|
25183
|
-
*
|
|
25184
|
-
*
|
|
25185
|
-
*
|
|
25182
|
+
* True when `pending` (a `state.pendingItemAction`) targets this exact
|
|
25183
|
+
* row. Action-agnostic on purpose — every surface + the sidebar render
|
|
25184
|
+
* the same spinner whether the row is being deleted or checked out, so
|
|
25185
|
+
* the spinner-swap test stays identical everywhere. Takes the field
|
|
25186
|
+
* value (not the whole state) so it can live next to the type without a
|
|
25187
|
+
* forward reference.
|
|
25186
25188
|
*/
|
|
25187
|
-
function
|
|
25189
|
+
function isPendingItemAction(pending, kind, id) {
|
|
25188
25190
|
return pending?.kind === kind && pending.id === id;
|
|
25189
25191
|
}
|
|
25190
25192
|
const DEFAULT_CHANGELOG_VIEW_STATE = {
|
|
@@ -26392,10 +26394,11 @@ function applyLogInkAction(state, action) {
|
|
|
26392
26394
|
workflowActionId: action.value ? undefined : state.workflowActionId,
|
|
26393
26395
|
pendingKey: undefined,
|
|
26394
26396
|
};
|
|
26395
|
-
case '
|
|
26396
|
-
// Pure marker for the in-flight
|
|
26397
|
-
//
|
|
26398
|
-
|
|
26397
|
+
case 'setPendingItemAction':
|
|
26398
|
+
// Pure marker for the in-flight row action (delete / checkout);
|
|
26399
|
+
// touches nothing else so the list keeps rendering normally
|
|
26400
|
+
// underneath the one spinner'd row.
|
|
26401
|
+
return { ...state, pendingItemAction: action.value };
|
|
26399
26402
|
case 'toggleFilterMode':
|
|
26400
26403
|
return {
|
|
26401
26404
|
...state,
|
|
@@ -31018,6 +31021,29 @@ function deleteBranch(git, branch, force = false) {
|
|
|
31018
31021
|
function isBranchNotFullyMergedError(message) {
|
|
31019
31022
|
return /not fully merged/i.test(message || '');
|
|
31020
31023
|
}
|
|
31024
|
+
/**
|
|
31025
|
+
* True when a branch delete was rejected because the branch is checked
|
|
31026
|
+
* out in a worktree. Unlike "not fully merged" there's no force escape
|
|
31027
|
+
* hatch — git refuses `git branch -D` on a worktree-checked-out branch
|
|
31028
|
+
* too — so the UI should surface a clear "free up the worktree first"
|
|
31029
|
+
* message rather than offering a force-delete that would fail the same
|
|
31030
|
+
* way. Matches git's wording: `Cannot delete branch 'x' checked out at
|
|
31031
|
+
* '<path>'` / `used by worktree at '<path>'`.
|
|
31032
|
+
*/
|
|
31033
|
+
function isBranchCheckedOutElsewhereError(message) {
|
|
31034
|
+
return /checked out at|used by worktree/i.test(message || '');
|
|
31035
|
+
}
|
|
31036
|
+
/**
|
|
31037
|
+
* Pull the worktree path out of git's "checked out at '<path>'" /
|
|
31038
|
+
* "used by worktree at '<path>'" rejection so the UI can name where the
|
|
31039
|
+
* branch is still in use. Returns undefined when the message doesn't
|
|
31040
|
+
* carry a path (older git phrasings) so callers can fall back to a
|
|
31041
|
+
* generic message.
|
|
31042
|
+
*/
|
|
31043
|
+
function parseCheckedOutWorktreePath(message) {
|
|
31044
|
+
const match = /(?:checked out at|used by worktree at) '([^']+)'/i.exec(message || '');
|
|
31045
|
+
return match?.[1];
|
|
31046
|
+
}
|
|
31021
31047
|
function fetchRemotes(git) {
|
|
31022
31048
|
return runAction$5(() => git.raw(['fetch', '--all', '--prune']), 'Fetched all remotes');
|
|
31023
31049
|
}
|
|
@@ -34429,7 +34455,7 @@ function renderActiveSidebarContent(h, Text, tab, state, context, contextStatus,
|
|
|
34429
34455
|
// shows this spinner in place of its leading marker (branches /
|
|
34430
34456
|
// worktrees) or appended to the row (tags / stashes, which have no
|
|
34431
34457
|
// leading status icon). `pending` is the single in-flight target.
|
|
34432
|
-
const pending = state.
|
|
34458
|
+
const pending = state.pendingItemAction;
|
|
34433
34459
|
const spin = inlineSpinnerGlyph(spinnerFrame, theme.ascii);
|
|
34434
34460
|
// Available rows for the active tab's list. The sidebar chrome
|
|
34435
34461
|
// takes ~10 rows (panel title + spacer + 5 tab headers + 4 inter-tab
|
|
@@ -34468,7 +34494,7 @@ function renderActiveSidebarContent(h, Text, tab, state, context, contextStatus,
|
|
|
34468
34494
|
return [
|
|
34469
34495
|
...headerRows,
|
|
34470
34496
|
...renderSelectableSidebarRows(h, Text, sortedBranches, state.selectedBranchIndex, focused, width, theme, (branch) => {
|
|
34471
|
-
const glyph =
|
|
34497
|
+
const glyph = isPendingItemAction(pending, 'branch', branch.shortName)
|
|
34472
34498
|
? spin
|
|
34473
34499
|
: branchRowMarker(branch, { ascii: theme.ascii }).glyph;
|
|
34474
34500
|
return `${glyph} ${branch.shortName}`;
|
|
@@ -34487,7 +34513,7 @@ function renderActiveSidebarContent(h, Text, tab, state, context, contextStatus,
|
|
|
34487
34513
|
const base = `${truncateCells(tag.name, 16)} ${tag.subject}`;
|
|
34488
34514
|
// Tags have no leading status icon, so the pending spinner is
|
|
34489
34515
|
// appended to the row instead of replacing a glyph.
|
|
34490
|
-
return
|
|
34516
|
+
return isPendingItemAction(pending, 'tag', tag.name) ? `${base} ${spin}` : base;
|
|
34491
34517
|
}, 'tab-tags', visibleListCount);
|
|
34492
34518
|
}
|
|
34493
34519
|
if (tab === 'stashes') {
|
|
@@ -34502,7 +34528,7 @@ function renderActiveSidebarContent(h, Text, tab, state, context, contextStatus,
|
|
|
34502
34528
|
const base = `@{${index}} ${stash.message || '(no message)'}`;
|
|
34503
34529
|
// `@{N}` is the stash ref, not a status icon, so append the
|
|
34504
34530
|
// spinner rather than replacing it.
|
|
34505
|
-
return
|
|
34531
|
+
return isPendingItemAction(pending, 'stash', stash.ref) ? `${base} ${spin}` : base;
|
|
34506
34532
|
}, 'tab-stashes', visibleListCount);
|
|
34507
34533
|
}
|
|
34508
34534
|
// worktrees
|
|
@@ -34514,7 +34540,7 @@ function renderActiveSidebarContent(h, Text, tab, state, context, contextStatus,
|
|
|
34514
34540
|
return [h(Text, { key: 'tab-worktrees-empty', dimColor: true }, ' No linked worktrees')];
|
|
34515
34541
|
}
|
|
34516
34542
|
return renderSelectableSidebarRows(h, Text, worktrees, state.selectedWorktreeListIndex, focused, width, theme, (worktree) => {
|
|
34517
|
-
const marker =
|
|
34543
|
+
const marker = isPendingItemAction(pending, 'worktree', worktree.path)
|
|
34518
34544
|
? spin
|
|
34519
34545
|
: worktree.current ? '*' : ' ';
|
|
34520
34546
|
const wstate = worktree.dirty ? 'dirty' : 'clean';
|
|
@@ -34950,7 +34976,7 @@ function renderBranchesSurface(ctx, spinnerFrame = 0) {
|
|
|
34950
34976
|
// While this branch's delete is in flight, its sync-state marker
|
|
34951
34977
|
// is replaced by an inline spinner (accent-coloured) so the row
|
|
34952
34978
|
// reads as "deleting" until it vanishes on refresh.
|
|
34953
|
-
const deleting =
|
|
34979
|
+
const deleting = isPendingItemAction(state.pendingItemAction, 'branch', branch.shortName);
|
|
34954
34980
|
const glyph = deleting ? inlineSpinnerGlyph(spinnerFrame, theme.ascii) : marker.glyph;
|
|
34955
34981
|
const glyphColor = deleting
|
|
34956
34982
|
? (theme.noColor ? undefined : theme.colors.accent)
|
|
@@ -38840,7 +38866,7 @@ function renderStashSurface(ctx, spinnerFrame = 0) {
|
|
|
38840
38866
|
// The `stash@{N}` ref is an identifier, not a status icon, so a
|
|
38841
38867
|
// delete-in-flight appends an accent spinner at the row's end
|
|
38842
38868
|
// (2 cells reserved from the width budget).
|
|
38843
|
-
const deleting =
|
|
38869
|
+
const deleting = isPendingItemAction(state.pendingItemAction, 'stash', stash.ref);
|
|
38844
38870
|
const spinnerSpan = deleting
|
|
38845
38871
|
? h(Text, { color: theme.noColor ? undefined : theme.colors.accent, dimColor: false }, ` ${inlineSpinnerGlyph(spinnerFrame, theme.ascii)}`)
|
|
38846
38872
|
: null;
|
|
@@ -39240,7 +39266,7 @@ function renderTagsSurface(ctx, spinnerFrame = 0) {
|
|
|
39240
39266
|
// Tags have no leading status icon, so a delete-in-flight appends
|
|
39241
39267
|
// an accent spinner at the row's end. Reserve its 2 cells from the
|
|
39242
39268
|
// truncation budget so it never pushes the row past the panel.
|
|
39243
|
-
const deleting =
|
|
39269
|
+
const deleting = isPendingItemAction(state.pendingItemAction, 'tag', tag.name);
|
|
39244
39270
|
const spinnerSpan = deleting
|
|
39245
39271
|
? h(Text, { color: theme.noColor ? undefined : theme.colors.accent, dimColor: false }, ` ${inlineSpinnerGlyph(spinnerFrame, theme.ascii)}`)
|
|
39246
39272
|
: null;
|
|
@@ -39322,7 +39348,7 @@ function renderWorktreesSurface(ctx, spinnerFrame = 0) {
|
|
|
39322
39348
|
const index = startIndex + offset;
|
|
39323
39349
|
const isSelected = index === selected;
|
|
39324
39350
|
const cursor = isSelected ? '>' : ' ';
|
|
39325
|
-
const marker =
|
|
39351
|
+
const marker = isPendingItemAction(state.pendingItemAction, 'worktree', entry.path)
|
|
39326
39352
|
? inlineSpinnerGlyph(spinnerFrame, theme.ascii)
|
|
39327
39353
|
: entry.current ? '*' : ' ';
|
|
39328
39354
|
const branchLabel = entry.branch ? entry.branch : entry.head || '<detached>';
|
|
@@ -40791,15 +40817,28 @@ const REMOTE_OP_LOADERS = {
|
|
|
40791
40817
|
* Returns `undefined` for non-delete workflows (and when nothing is
|
|
40792
40818
|
* selected), which the runner treats as "no pending marker".
|
|
40793
40819
|
*/
|
|
40794
|
-
function
|
|
40820
|
+
function resolvePendingItemAction(id, state, context) {
|
|
40795
40821
|
const { filter } = state;
|
|
40822
|
+
// Checking out a branch gets the same inline spinner on its row as a
|
|
40823
|
+
// delete does — the action just runs `git checkout` instead of
|
|
40824
|
+
// `git branch -d`. Resolved the same way as the delete branch case
|
|
40825
|
+
// (and identically to the checkout-branch handler) so the spinner
|
|
40826
|
+
// lands on exactly the row the user pressed enter on.
|
|
40827
|
+
if (id === 'checkout-branch') {
|
|
40828
|
+
const all = sortBranches(context.branches?.localBranches || [], state.branchSort);
|
|
40829
|
+
const visible = filter
|
|
40830
|
+
? all.filter((b) => matchesPromotedFilter([b.shortName, b.upstream || ''], filter))
|
|
40831
|
+
: all;
|
|
40832
|
+
const branch = visible[Math.min(state.selectedBranchIndex, visible.length - 1)];
|
|
40833
|
+
return branch ? { kind: 'branch', id: branch.shortName, action: 'checkout' } : undefined;
|
|
40834
|
+
}
|
|
40796
40835
|
if (id === 'delete-branch' || id === 'force-delete-branch') {
|
|
40797
40836
|
const all = sortBranches(context.branches?.localBranches || [], state.branchSort);
|
|
40798
40837
|
const visible = filter
|
|
40799
40838
|
? all.filter((b) => matchesPromotedFilter([b.shortName, b.upstream || ''], filter))
|
|
40800
40839
|
: all;
|
|
40801
40840
|
const branch = visible[Math.min(state.selectedBranchIndex, visible.length - 1)];
|
|
40802
|
-
return branch ? { kind: 'branch', id: branch.shortName } : undefined;
|
|
40841
|
+
return branch ? { kind: 'branch', id: branch.shortName, action: 'delete' } : undefined;
|
|
40803
40842
|
}
|
|
40804
40843
|
if (id === 'delete-tag') {
|
|
40805
40844
|
const all = sortTags(context.tags?.tags || [], state.tagSort);
|
|
@@ -40807,7 +40846,7 @@ function resolvePendingDeletion(id, state, context) {
|
|
|
40807
40846
|
? all.filter((t) => matchesPromotedFilter([t.name, t.subject], filter))
|
|
40808
40847
|
: all;
|
|
40809
40848
|
const tag = visible[Math.min(state.selectedTagIndex, visible.length - 1)];
|
|
40810
|
-
return tag ? { kind: 'tag', id: tag.name } : undefined;
|
|
40849
|
+
return tag ? { kind: 'tag', id: tag.name, action: 'delete' } : undefined;
|
|
40811
40850
|
}
|
|
40812
40851
|
if (id === 'drop-stash') {
|
|
40813
40852
|
const all = context.stashes?.stashes || [];
|
|
@@ -40815,7 +40854,7 @@ function resolvePendingDeletion(id, state, context) {
|
|
|
40815
40854
|
? all.filter((s) => matchesPromotedFilter([s.ref, s.message], filter))
|
|
40816
40855
|
: all;
|
|
40817
40856
|
const stash = visible[Math.min(state.selectedStashIndex, visible.length - 1)];
|
|
40818
|
-
return stash ? { kind: 'stash', id: stash.ref } : undefined;
|
|
40857
|
+
return stash ? { kind: 'stash', id: stash.ref, action: 'delete' } : undefined;
|
|
40819
40858
|
}
|
|
40820
40859
|
if (id === 'remove-worktree') {
|
|
40821
40860
|
const all = context.worktreeList?.worktrees || [];
|
|
@@ -40825,7 +40864,7 @@ function resolvePendingDeletion(id, state, context) {
|
|
|
40825
40864
|
const wt = visible.length
|
|
40826
40865
|
? visible[Math.min(state.selectedWorktreeListIndex, visible.length - 1)]
|
|
40827
40866
|
: all[Math.min(state.selectedWorktreeListIndex, Math.max(0, all.length - 1))];
|
|
40828
|
-
return wt ? { kind: 'worktree', id: wt.path } : undefined;
|
|
40867
|
+
return wt ? { kind: 'worktree', id: wt.path, action: 'delete' } : undefined;
|
|
40829
40868
|
}
|
|
40830
40869
|
return undefined;
|
|
40831
40870
|
}
|
|
@@ -41146,9 +41185,10 @@ function LogInkApp(deps) {
|
|
|
41146
41185
|
state.commitCompose.loading ||
|
|
41147
41186
|
Boolean(state.remoteOp) ||
|
|
41148
41187
|
Boolean(state.statusLoading) ||
|
|
41149
|
-
// Keep the shared spinner ticking while a list-item delete
|
|
41150
|
-
// flight so its inline pending glyph animates
|
|
41151
|
-
|
|
41188
|
+
// Keep the shared spinner ticking while a list-item action (delete
|
|
41189
|
+
// or checkout) is in flight so its inline pending glyph animates
|
|
41190
|
+
// instead of freezing.
|
|
41191
|
+
Boolean(state.pendingItemAction);
|
|
41152
41192
|
React.useEffect(() => {
|
|
41153
41193
|
if (!anyLoading) {
|
|
41154
41194
|
// Reset to 0 so the next loading state starts from a known
|
|
@@ -44088,14 +44128,15 @@ function LogInkApp(deps) {
|
|
|
44088
44128
|
if (remoteOp) {
|
|
44089
44129
|
dispatch({ type: 'setRemoteOp', value: remoteOp });
|
|
44090
44130
|
}
|
|
44091
|
-
// Mark the cursored row as
|
|
44092
|
-
// spinner while the git call runs. Cleared in
|
|
44093
|
-
// refresh, so a successful delete hands straight
|
|
44094
|
-
// vanishing,
|
|
44095
|
-
// the
|
|
44096
|
-
|
|
44097
|
-
|
|
44098
|
-
|
|
44131
|
+
// Mark the cursored row as busy so it shows an inline pending
|
|
44132
|
+
// spinner while the git call runs (delete or checkout). Cleared in
|
|
44133
|
+
// `finally` after the refresh, so a successful delete hands straight
|
|
44134
|
+
// off to the row vanishing, a checkout to the sidebar repainting
|
|
44135
|
+
// with the new current branch, and a failure (e.g. an unmerged
|
|
44136
|
+
// branch) restores the row's normal icon alongside the error status.
|
|
44137
|
+
const pendingItemAction = resolvePendingItemAction(id, state, context);
|
|
44138
|
+
if (pendingItemAction) {
|
|
44139
|
+
dispatch({ type: 'setPendingItemAction', value: pendingItemAction });
|
|
44099
44140
|
}
|
|
44100
44141
|
try {
|
|
44101
44142
|
const result = await handler();
|
|
@@ -44108,6 +44149,24 @@ function LogInkApp(deps) {
|
|
|
44108
44149
|
if (id === 'delete-branch' && !result?.ok && isBranchNotFullyMergedError(result?.message)) {
|
|
44109
44150
|
dispatch({ type: 'setPendingConfirmation', value: 'force-delete-branch' });
|
|
44110
44151
|
}
|
|
44152
|
+
// A branch checked out in a worktree can't be deleted — and unlike
|
|
44153
|
+
// the unmerged case, `git branch -D` won't force it either, so we
|
|
44154
|
+
// don't offer a confirmation. Replace git's raw rejection with a
|
|
44155
|
+
// clear "free up the worktree first" message that names where the
|
|
44156
|
+
// branch is still in use.
|
|
44157
|
+
if ((id === 'delete-branch' || id === 'force-delete-branch') &&
|
|
44158
|
+
!result?.ok &&
|
|
44159
|
+
isBranchCheckedOutElsewhereError(result?.message)) {
|
|
44160
|
+
const worktreePath = parseCheckedOutWorktreePath(result?.message);
|
|
44161
|
+
const branchName = pendingItemAction?.id;
|
|
44162
|
+
dispatch({
|
|
44163
|
+
type: 'setStatus',
|
|
44164
|
+
value: worktreePath
|
|
44165
|
+
? `Can't delete ${branchName ? `'${branchName}'` : 'branch'} — checked out in worktree ${worktreePath}. Switch that worktree off the branch or remove it first.`
|
|
44166
|
+
: `Can't delete ${branchName ? `'${branchName}'` : 'branch'} — it's checked out in another worktree. Switch that worktree off the branch or remove it first.`,
|
|
44167
|
+
kind: 'warning',
|
|
44168
|
+
});
|
|
44169
|
+
}
|
|
44111
44170
|
// Refresh history rows AS WELL when the workflow could have
|
|
44112
44171
|
// changed the commits the user sees (#945 follow-up). The
|
|
44113
44172
|
// workflow IDs below all either create/rewrite local commits or
|
|
@@ -44143,15 +44202,18 @@ function LogInkApp(deps) {
|
|
|
44143
44202
|
if (result?.ok && historyMutatingIds.has(id)) {
|
|
44144
44203
|
await refreshHistoryRows();
|
|
44145
44204
|
}
|
|
44146
|
-
// Checkout-branch
|
|
44147
|
-
// refresh
|
|
44148
|
-
//
|
|
44149
|
-
//
|
|
44150
|
-
//
|
|
44151
|
-
// the
|
|
44205
|
+
// Checkout-branch snaps the cursor to position 0 first so when the
|
|
44206
|
+
// refresh completes and the new current branch lands at the top
|
|
44207
|
+
// (per #809's pin-current rule), the cursor is already there
|
|
44208
|
+
// waiting. The refresh is *silent*: the loud refresh used to blank
|
|
44209
|
+
// every branch name behind a "loading branches…" placeholder (#806),
|
|
44210
|
+
// but the in-flight row now carries its own inline pending spinner
|
|
44211
|
+
// (resolvePendingItemAction → action 'checkout'), so a silent
|
|
44212
|
+
// stale-while-revalidate swap keeps the list readable and just
|
|
44213
|
+
// repaints the current-branch marker once the new context lands.
|
|
44152
44214
|
if (id === 'checkout-branch' && result?.ok) {
|
|
44153
44215
|
dispatch({ type: 'resetBranchSelection' });
|
|
44154
|
-
await refreshContext();
|
|
44216
|
+
await refreshContext({ silent: true });
|
|
44155
44217
|
}
|
|
44156
44218
|
else {
|
|
44157
44219
|
// Silent refresh so the deleted item disappears from the list
|
|
@@ -44206,11 +44268,11 @@ function LogInkApp(deps) {
|
|
|
44206
44268
|
if (remoteOp) {
|
|
44207
44269
|
dispatch({ type: 'setRemoteOp', value: undefined });
|
|
44208
44270
|
}
|
|
44209
|
-
// Same guarantee for the per-row
|
|
44210
|
-
// the
|
|
44211
|
-
// left spinning forever.
|
|
44212
|
-
if (
|
|
44213
|
-
dispatch({ type: '
|
|
44271
|
+
// Same guarantee for the per-row pending spinner (delete or
|
|
44272
|
+
// checkout): clear it whether the action succeeded, failed, or the
|
|
44273
|
+
// refresh threw, so no row is left spinning forever.
|
|
44274
|
+
if (pendingItemAction) {
|
|
44275
|
+
dispatch({ type: 'setPendingItemAction', value: undefined });
|
|
44214
44276
|
}
|
|
44215
44277
|
}
|
|
44216
44278
|
}, [context, dispatch, git, refreshContext, refreshHistoryRows, refreshWorktreeContext,
|