git-coco 0.52.0 → 0.53.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 +322 -36
- package/dist/index.js +322 -36
- package/package.json +3 -3
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.
|
|
64
|
+
const BUILD_VERSION = "0.53.0";
|
|
65
65
|
|
|
66
66
|
const isInteractive = (config) => {
|
|
67
67
|
return config?.mode === 'interactive' || !!config?.interactive;
|
|
@@ -15612,6 +15612,7 @@ Structural rules:
|
|
|
15612
15612
|
- Only use hunk IDs LITERALLY copied from the "Staged hunk inventory" section below. Do not invent or guess hunk IDs.
|
|
15613
15613
|
- If the hunk inventory says "No hunk-level inventory available" then EVERY group's "hunks" array MUST be empty (use only "files"). Do not write hunk IDs like "path::hunk-1" when no hunk inventory exists — those are not valid.
|
|
15614
15614
|
- Prefer 2-5 commits unless the changes are truly all one topic.
|
|
15615
|
+
- Order the groups in the sequence they would logically be built — foundational changes first, consumers after. If group B uses a symbol, function, type, or file introduced in group A, A MUST appear before B in the array. The applier commits in array order, so this order becomes the git history. Example: a "feat: add helpers" group that introduces \`formatX()\` must come before a "feat: wire helpers into renderer" group that calls \`formatX()\`, even if the staged diff is presented in the opposite order. When two groups have no dependency relationship, prefer the one closer to a "scaffold" (types, config, new files) before the one closer to a "use site" (existing files modified to consume the new code).
|
|
15615
15616
|
|
|
15616
15617
|
Commit message style:
|
|
15617
15618
|
- Write each "title" in the imperative mood ("add", not "added"), under 72 chars.
|
|
@@ -21068,6 +21069,37 @@ function getLogInkWorkflowActions() {
|
|
|
21068
21069
|
kind: 'normal',
|
|
21069
21070
|
requiresConfirmation: false,
|
|
21070
21071
|
},
|
|
21072
|
+
// Per-view variants of fetch / pull / push that act on the
|
|
21073
|
+
// cursored branch instead of the current one. Empty `key` keeps
|
|
21074
|
+
// them palette-discoverable without registering a global hotkey —
|
|
21075
|
+
// inkInput.ts dispatches them contextually when the user presses
|
|
21076
|
+
// F / U / P while the branches sidebar is focused. Outside that
|
|
21077
|
+
// context, the F / U / P keys still fire the global *-current-*
|
|
21078
|
+
// / fetch-remotes variants above.
|
|
21079
|
+
{
|
|
21080
|
+
id: 'fetch-selected-branch',
|
|
21081
|
+
key: '',
|
|
21082
|
+
label: 'Fetch selected branch',
|
|
21083
|
+
description: 'Run `git fetch <remote> <branch>` for the cursored branch in the branches view / sidebar.',
|
|
21084
|
+
kind: 'normal',
|
|
21085
|
+
requiresConfirmation: false,
|
|
21086
|
+
},
|
|
21087
|
+
{
|
|
21088
|
+
id: 'pull-selected-branch',
|
|
21089
|
+
key: '',
|
|
21090
|
+
label: 'Pull selected branch',
|
|
21091
|
+
description: 'Pull the cursored branch in the branches view / sidebar. Falls back to a fast-forward-only refspec fetch when the branch is not currently checked out; refuses non-FF.',
|
|
21092
|
+
kind: 'normal',
|
|
21093
|
+
requiresConfirmation: false,
|
|
21094
|
+
},
|
|
21095
|
+
{
|
|
21096
|
+
id: 'push-selected-branch',
|
|
21097
|
+
key: '',
|
|
21098
|
+
label: 'Push selected branch',
|
|
21099
|
+
description: 'Run `git push <remote> <branch>` for the cursored branch in the branches view / sidebar.',
|
|
21100
|
+
kind: 'normal',
|
|
21101
|
+
requiresConfirmation: false,
|
|
21102
|
+
},
|
|
21071
21103
|
{
|
|
21072
21104
|
// Per-view-only — the inkInput handler scopes this to the tags
|
|
21073
21105
|
// surface so we don't expose `R` as a remote-delete from elsewhere.
|
|
@@ -22008,8 +22040,22 @@ function getLogInkFooterHints(options) {
|
|
|
22008
22040
|
// "enter open" hint that drills into the dedicated view.
|
|
22009
22041
|
const itemsPresent = (options.sidebarItemCount ?? 0) > 0;
|
|
22010
22042
|
if (itemsPresent && options.sidebarTab === 'branches') {
|
|
22043
|
+
// P / U / F fire the global pull-current-branch, push-current-branch,
|
|
22044
|
+
// fetch-remotes workflows — already implemented, just not visible in
|
|
22045
|
+
// the footer before. Surfacing them here matters because the user's
|
|
22046
|
+
// attention is on a branch when the branches sidebar is focused;
|
|
22047
|
+
// pull / push / fetch are the next obvious actions.
|
|
22048
|
+
//
|
|
22049
|
+
// Note: `U` and `P` currently operate on the CURRENT branch, not the
|
|
22050
|
+
// cursored one. Task #5 will extend them to act on the cursored row;
|
|
22051
|
+
// until then the labels read as "current-branch ops" by virtue of
|
|
22052
|
+
// matching the workflow descriptions.
|
|
22011
22053
|
return {
|
|
22012
|
-
contextual: [
|
|
22054
|
+
contextual: [
|
|
22055
|
+
'↑/↓ branches', '←/→ tab', 'enter checkout',
|
|
22056
|
+
'F fetch', 'U pull', 'P push',
|
|
22057
|
+
'D delete', 'R rename', 'u upstream',
|
|
22058
|
+
],
|
|
22013
22059
|
global: NORMAL_GLOBAL_HINTS,
|
|
22014
22060
|
};
|
|
22015
22061
|
}
|
|
@@ -26438,6 +26484,27 @@ function getLogInkInputEvents(state, inputValue, key = {}, context = {}) {
|
|
|
26438
26484
|
events.push({ type: 'createManualCommit' });
|
|
26439
26485
|
return events;
|
|
26440
26486
|
}
|
|
26487
|
+
// Context-sensitive per-branch variants of F / U / P. When the
|
|
26488
|
+
// user has the branches sidebar / view focused with at least one
|
|
26489
|
+
// branch, F / U / P should act on the cursored row, not on the
|
|
26490
|
+
// current branch. This intercept fires BEFORE the generic
|
|
26491
|
+
// workflow-by-key lookup below so the global *-current-branch
|
|
26492
|
+
// variants don't shadow the contextual ones.
|
|
26493
|
+
//
|
|
26494
|
+
// Outside the branches context, the generic lookup runs and the
|
|
26495
|
+
// F / U / P keys hit the global `fetch-remotes` / `pull-current-branch`
|
|
26496
|
+
// / `push-current-branch` workflows as before.
|
|
26497
|
+
if (isBranchActionTarget(state) && context.branchCount) {
|
|
26498
|
+
if (inputValue === 'F') {
|
|
26499
|
+
return [{ type: 'runWorkflowAction', id: 'fetch-selected-branch' }];
|
|
26500
|
+
}
|
|
26501
|
+
if (inputValue === 'U') {
|
|
26502
|
+
return [{ type: 'runWorkflowAction', id: 'pull-selected-branch' }];
|
|
26503
|
+
}
|
|
26504
|
+
if (inputValue === 'P') {
|
|
26505
|
+
return [{ type: 'runWorkflowAction', id: 'push-selected-branch' }];
|
|
26506
|
+
}
|
|
26507
|
+
}
|
|
26441
26508
|
const workflowAction = getLogInkWorkflowActionByKey(inputValue);
|
|
26442
26509
|
if (workflowAction?.requiresConfirmation) {
|
|
26443
26510
|
return [action({ type: 'setPendingConfirmation', value: workflowAction.id })];
|
|
@@ -27540,6 +27607,106 @@ function pushCurrentBranch(git) {
|
|
|
27540
27607
|
function setUpstream(git, localBranch, upstreamBranch) {
|
|
27541
27608
|
return runAction$5(() => git.raw(['branch', '--set-upstream-to', upstreamBranch, localBranch]), `Set ${localBranch} upstream to ${upstreamBranch}`);
|
|
27542
27609
|
}
|
|
27610
|
+
/**
|
|
27611
|
+
* Push an arbitrary local branch (need not be the current branch) to
|
|
27612
|
+
* its remote. Refuses when the branch has no upstream and no remote
|
|
27613
|
+
* defaulting is configured — that branch needs a `git push -u …` from
|
|
27614
|
+
* the shell first.
|
|
27615
|
+
*
|
|
27616
|
+
* Pairs with `pushCurrentBranch` (no-arg variant); the workstation
|
|
27617
|
+
* dispatcher picks one or the other based on where the cursor is.
|
|
27618
|
+
*/
|
|
27619
|
+
function pushBranch(git, branch) {
|
|
27620
|
+
if (branch.type !== 'local') {
|
|
27621
|
+
return Promise.resolve({
|
|
27622
|
+
ok: false,
|
|
27623
|
+
message: 'Only local branches can be pushed.',
|
|
27624
|
+
});
|
|
27625
|
+
}
|
|
27626
|
+
if (!branch.upstream || !branch.remote) {
|
|
27627
|
+
return Promise.resolve({
|
|
27628
|
+
ok: false,
|
|
27629
|
+
message: `${branch.shortName} has no upstream — checkout the branch and run \`git push -u <remote> ${branch.shortName}\` first.`,
|
|
27630
|
+
});
|
|
27631
|
+
}
|
|
27632
|
+
return runAction$5(() => git.raw(['push', branch.remote, branch.shortName]), `Pushed ${branch.shortName} to ${branch.upstream}`);
|
|
27633
|
+
}
|
|
27634
|
+
/**
|
|
27635
|
+
* Fetch the cursored branch's upstream from its remote. Side-effect
|
|
27636
|
+
* free on the working tree — just updates the remote-tracking ref.
|
|
27637
|
+
* Works for any branch with an upstream regardless of checkout state.
|
|
27638
|
+
*
|
|
27639
|
+
* Falls back to a clean error when the branch has no upstream
|
|
27640
|
+
* configured (`git fetch <remote> <name>` would assume an unrelated
|
|
27641
|
+
* default refspec and surprise the user).
|
|
27642
|
+
*/
|
|
27643
|
+
function fetchBranch(git, branch) {
|
|
27644
|
+
if (branch.type !== 'local') {
|
|
27645
|
+
return Promise.resolve({
|
|
27646
|
+
ok: false,
|
|
27647
|
+
message: 'Only local branches can be fetched per-branch — use F to fetch all remotes.',
|
|
27648
|
+
});
|
|
27649
|
+
}
|
|
27650
|
+
if (!branch.upstream || !branch.remote) {
|
|
27651
|
+
return Promise.resolve({
|
|
27652
|
+
ok: false,
|
|
27653
|
+
message: `${branch.shortName} has no upstream — nothing to fetch.`,
|
|
27654
|
+
});
|
|
27655
|
+
}
|
|
27656
|
+
// `branch.upstream` is the short form (e.g. `origin/main`); the
|
|
27657
|
+
// ref name after the remote prefix is what fetch wants as the
|
|
27658
|
+
// refspec source. For a remote `origin` and upstream `origin/main`
|
|
27659
|
+
// we run `git fetch origin main`.
|
|
27660
|
+
const upstreamRef = branch.upstream.startsWith(`${branch.remote}/`)
|
|
27661
|
+
? branch.upstream.slice(branch.remote.length + 1)
|
|
27662
|
+
: branch.upstream;
|
|
27663
|
+
return runAction$5(() => git.raw(['fetch', branch.remote, upstreamRef]), `Fetched ${branch.upstream}`);
|
|
27664
|
+
}
|
|
27665
|
+
/**
|
|
27666
|
+
* Pull the cursored branch. Branches into two paths based on whether
|
|
27667
|
+
* the branch is currently checked out:
|
|
27668
|
+
*
|
|
27669
|
+
* - **Current branch**: defer to `pullCurrentBranch` (standard
|
|
27670
|
+
* `git pull --ff-only`).
|
|
27671
|
+
* - **Non-current branch**: use the refspec form
|
|
27672
|
+
* `git fetch <remote> <branch>:<branch>` which advances the local
|
|
27673
|
+
* ref to match the remote ref ONLY if the update is fast-forward.
|
|
27674
|
+
* Returns non-zero on non-FF without touching the working tree.
|
|
27675
|
+
* Diverged branches need a checkout + `pull --rebase` from the
|
|
27676
|
+
* user; we refuse rather than try to do that for them.
|
|
27677
|
+
*
|
|
27678
|
+
* `currentBranchName` lets the dispatcher compare without re-querying
|
|
27679
|
+
* git — it already has the value in `context.branches.currentBranch`.
|
|
27680
|
+
*/
|
|
27681
|
+
function pullBranch(git, branch, currentBranchName) {
|
|
27682
|
+
if (branch.type !== 'local') {
|
|
27683
|
+
return Promise.resolve({
|
|
27684
|
+
ok: false,
|
|
27685
|
+
message: 'Only local branches can be pulled.',
|
|
27686
|
+
});
|
|
27687
|
+
}
|
|
27688
|
+
if (!branch.upstream || !branch.remote) {
|
|
27689
|
+
return Promise.resolve({
|
|
27690
|
+
ok: false,
|
|
27691
|
+
message: `${branch.shortName} has no upstream — nothing to pull.`,
|
|
27692
|
+
});
|
|
27693
|
+
}
|
|
27694
|
+
// Current branch — defer to the in-place workflow.
|
|
27695
|
+
if (branch.shortName === currentBranchName) {
|
|
27696
|
+
return pullCurrentBranch(git);
|
|
27697
|
+
}
|
|
27698
|
+
// Non-current branch — refspec-based fast-forward refusing non-FF.
|
|
27699
|
+
// `branch.upstream` is `<remote>/<ref>`; strip the remote prefix to
|
|
27700
|
+
// get the upstream ref name to fetch.
|
|
27701
|
+
const upstreamRef = branch.upstream.startsWith(`${branch.remote}/`)
|
|
27702
|
+
? branch.upstream.slice(branch.remote.length + 1)
|
|
27703
|
+
: branch.upstream;
|
|
27704
|
+
return runAction$5(() => git.raw([
|
|
27705
|
+
'fetch',
|
|
27706
|
+
branch.remote,
|
|
27707
|
+
`${upstreamRef}:${branch.shortName}`,
|
|
27708
|
+
]), `Fast-forwarded ${branch.shortName} to ${branch.upstream}`);
|
|
27709
|
+
}
|
|
27543
27710
|
|
|
27544
27711
|
async function runAction$4(action, successMessage) {
|
|
27545
27712
|
try {
|
|
@@ -29063,29 +29230,81 @@ function formatBranchDivergence(branch, options = {}) {
|
|
|
29063
29230
|
parts.push(`↓${branch.behind}`);
|
|
29064
29231
|
return `${parts.join(' ')} ${branch.upstream}`;
|
|
29065
29232
|
}
|
|
29066
|
-
|
|
29067
|
-
|
|
29068
|
-
|
|
29069
|
-
|
|
29070
|
-
|
|
29071
|
-
|
|
29072
|
-
|
|
29073
|
-
|
|
29074
|
-
|
|
29075
|
-
|
|
29076
|
-
|
|
29077
|
-
|
|
29233
|
+
function formatUpstreamAheadBanner(branch, options = {}) {
|
|
29234
|
+
if (!branch?.upstream || branch.behind <= 0) {
|
|
29235
|
+
return undefined;
|
|
29236
|
+
}
|
|
29237
|
+
const sep = options.ascii ? '.' : '·';
|
|
29238
|
+
if (branch.ahead > 0) {
|
|
29239
|
+
// Diverged — local has work too, fast-forward pull is impossible.
|
|
29240
|
+
// Suggest pull --rebase as the cleaner-history default; users who
|
|
29241
|
+
// prefer merge can do that themselves.
|
|
29242
|
+
const symbols = options.ascii
|
|
29243
|
+
? `+${branch.ahead} -${branch.behind}`
|
|
29244
|
+
: `↑${branch.ahead} ↓${branch.behind}`;
|
|
29245
|
+
return `${symbols} diverged from ${branch.upstream} ${sep} F fetch ${sep} U pull --rebase`;
|
|
29246
|
+
}
|
|
29247
|
+
// Behind-only — fast-forward pull works.
|
|
29248
|
+
const arrow = options.ascii ? 'v' : '↓';
|
|
29249
|
+
const noun = branch.behind === 1 ? 'commit' : 'commits';
|
|
29250
|
+
return `${arrow} ${branch.behind} ${noun} behind ${branch.upstream} ${sep} F fetch ${sep} U pull`;
|
|
29251
|
+
}
|
|
29078
29252
|
function branchRowMarker(branch, options = {}) {
|
|
29079
|
-
if (branch.current)
|
|
29080
|
-
return '*';
|
|
29081
|
-
|
|
29082
|
-
|
|
29253
|
+
if (branch.current) {
|
|
29254
|
+
return { glyph: '*', kind: 'head' };
|
|
29255
|
+
}
|
|
29256
|
+
if (!branch.upstream) {
|
|
29257
|
+
return { glyph: options.ascii ? '?' : '◌', kind: 'no-upstream' };
|
|
29258
|
+
}
|
|
29083
29259
|
const ahead = branch.ahead ?? 0;
|
|
29084
29260
|
const behind = branch.behind ?? 0;
|
|
29085
29261
|
if (ahead === 0 && behind === 0) {
|
|
29086
|
-
return options.ascii ? '=' : '≡';
|
|
29262
|
+
return { glyph: options.ascii ? '=' : '≡', kind: 'synced' };
|
|
29263
|
+
}
|
|
29264
|
+
if (ahead > 0 && behind > 0) {
|
|
29265
|
+
return { glyph: options.ascii ? '~' : '⇅', kind: 'diverged' };
|
|
29266
|
+
}
|
|
29267
|
+
if (behind > 0) {
|
|
29268
|
+
return { glyph: options.ascii ? 'v' : '↓', kind: 'behind' };
|
|
29269
|
+
}
|
|
29270
|
+
// ahead > 0 (the only remaining case after the guards above)
|
|
29271
|
+
return { glyph: options.ascii ? '^' : '↑', kind: 'ahead' };
|
|
29272
|
+
}
|
|
29273
|
+
/**
|
|
29274
|
+
* Theme-aware colour picker for a `BranchRowMarker.kind`.
|
|
29275
|
+
*
|
|
29276
|
+
* Reuses the existing chip / banner colour semantic so the workstation
|
|
29277
|
+
* speaks one visual language across history (chips, "behind upstream"
|
|
29278
|
+
* banner) and the branches list:
|
|
29279
|
+
*
|
|
29280
|
+
* - `head` → success green (matches HEAD chip)
|
|
29281
|
+
* - `behind` → warning yellow (matches "behind upstream" banner)
|
|
29282
|
+
* - `diverged` → warning yellow (same: action needed inbound)
|
|
29283
|
+
* - `ahead` → info blue (you have work to push)
|
|
29284
|
+
* - `synced` → undefined (neutral; inherit row's existing dim)
|
|
29285
|
+
* - `no-upstream` → undefined (neutral; same)
|
|
29286
|
+
*
|
|
29287
|
+
* Returns `undefined` under `noColor` / `ascii` for the muted cases so
|
|
29288
|
+
* the row renderer skips the colour wrap entirely; the glyph alone
|
|
29289
|
+
* carries the meaning.
|
|
29290
|
+
*/
|
|
29291
|
+
function getBranchRowMarkerColor(kind, theme) {
|
|
29292
|
+
if (theme.noColor)
|
|
29293
|
+
return undefined;
|
|
29294
|
+
switch (kind) {
|
|
29295
|
+
case 'head':
|
|
29296
|
+
return theme.colors.success;
|
|
29297
|
+
case 'behind':
|
|
29298
|
+
case 'diverged':
|
|
29299
|
+
return theme.colors.warning;
|
|
29300
|
+
case 'ahead':
|
|
29301
|
+
return theme.colors.info;
|
|
29302
|
+
case 'synced':
|
|
29303
|
+
case 'no-upstream':
|
|
29304
|
+
return undefined;
|
|
29305
|
+
default:
|
|
29306
|
+
return undefined;
|
|
29087
29307
|
}
|
|
29088
|
-
return options.ascii ? '~' : '↕';
|
|
29089
29308
|
}
|
|
29090
29309
|
/**
|
|
29091
29310
|
* Compact, human-friendly relative timestamp for the branch row.
|
|
@@ -29693,7 +29912,7 @@ function renderActiveSidebarContent(h, Text, tab, state, context, contextStatus,
|
|
|
29693
29912
|
];
|
|
29694
29913
|
return [
|
|
29695
29914
|
...headerRows,
|
|
29696
|
-
...renderSelectableSidebarRows(h, Text, sortedBranches, state.selectedBranchIndex, focused, width, theme, (branch) => `${branchRowMarker(branch, { ascii: theme.ascii })} ${branch.shortName}`, 'tab-branches', visibleListCount),
|
|
29915
|
+
...renderSelectableSidebarRows(h, Text, sortedBranches, state.selectedBranchIndex, focused, width, theme, (branch) => `${branchRowMarker(branch, { ascii: theme.ascii }).glyph} ${branch.shortName}`, 'tab-branches', visibleListCount),
|
|
29697
29916
|
];
|
|
29698
29917
|
}
|
|
29699
29918
|
if (tab === 'tags') {
|
|
@@ -30156,21 +30375,22 @@ function renderBranchesSurface(h, components, state, context, contextStatus, bod
|
|
|
30156
30375
|
const isSelected = index === selected;
|
|
30157
30376
|
const cursor = isSelected ? '>' : ' ';
|
|
30158
30377
|
const marker = branchRowMarker(branch, { ascii: theme.ascii });
|
|
30378
|
+
const markerColor = getBranchRowMarkerColor(marker.kind, theme);
|
|
30159
30379
|
const divergence = formatBranchDivergence(branch, { ascii: theme.ascii });
|
|
30160
30380
|
const lastTouched = formatBranchLastTouched(branch.date, new Date());
|
|
30161
30381
|
// Split the row into spans so the timestamp stays dim even on the
|
|
30162
|
-
// currently-selected (bold) row
|
|
30163
|
-
//
|
|
30164
|
-
// right-padded so the divergence column stays aligned across rows.
|
|
30382
|
+
// currently-selected (bold) row, and the sync-state marker keeps
|
|
30383
|
+
// its own colour even when the surrounding row text is dimmed.
|
|
30165
30384
|
const namePadded = truncateCells(branch.shortName, nameColWidth).padEnd(nameColWidth);
|
|
30166
30385
|
const timestampPadded = lastTouched.padEnd(8);
|
|
30167
30386
|
const lineDim = !isSelected && !branch.current;
|
|
30168
|
-
const
|
|
30387
|
+
const cursorAndPad = `${cursor} `;
|
|
30388
|
+
const trailingName = ` ${namePadded} `;
|
|
30169
30389
|
const trailingDivergence = divergence ? ` ${divergence}` : '';
|
|
30170
30390
|
// Truncate the assembled line to the actual panel width so a
|
|
30171
30391
|
// narrow inspector / sidebar focus doesn't push branch rows
|
|
30172
30392
|
// onto a second visual line (#830).
|
|
30173
|
-
const fullText = `${
|
|
30393
|
+
const fullText = `${cursorAndPad}${marker.glyph}${trailingName}${timestampPadded}${trailingDivergence}`;
|
|
30174
30394
|
const truncated = truncateCells(fullText, Math.max(20, width - 4));
|
|
30175
30395
|
// If truncation chopped into the timestamp/divergence portion,
|
|
30176
30396
|
// fall back to a single Text to keep the visible width honest.
|
|
@@ -30185,7 +30405,15 @@ function renderBranchesSurface(h, components, state, context, contextStatus, bod
|
|
|
30185
30405
|
key: `branch-${index}`,
|
|
30186
30406
|
bold: isSelected,
|
|
30187
30407
|
dimColor: lineDim,
|
|
30188
|
-
},
|
|
30408
|
+
}, cursorAndPad,
|
|
30409
|
+
// The marker carries the sync-state colour; an explicit
|
|
30410
|
+
// `dimColor: false` on this span keeps the colour bright even
|
|
30411
|
+
// when the surrounding row is dim (other branches in the list
|
|
30412
|
+
// dim out under the existing `lineDim` rule). The synced /
|
|
30413
|
+
// no-upstream kinds return undefined from
|
|
30414
|
+
// `getBranchRowMarkerColor`, so those markers inherit the
|
|
30415
|
+
// row's dim and read as quiet chrome.
|
|
30416
|
+
h(Text, { color: markerColor, dimColor: markerColor ? false : undefined }, marker.glyph), trailingName, h(Text, { dimColor: true }, timestampPadded), trailingDivergence);
|
|
30189
30417
|
});
|
|
30190
30418
|
return h(Box, {
|
|
30191
30419
|
borderColor: focusBorderColor(theme, focused),
|
|
@@ -32197,6 +32425,25 @@ function renderHistoryPanel(h, components, state, context, bodyRows, width, them
|
|
|
32197
32425
|
paddingX: 1,
|
|
32198
32426
|
width,
|
|
32199
32427
|
}, h(Box, { justifyContent: 'space-between' }, h(Text, { bold: true }, panelTitle('Commits', focused)), h(Text, { dimColor: true }, `${title} | ${graphMode} | ${loadState}`)),
|
|
32428
|
+
// Upstream-ahead banner. Surfaces "the remote has work you don't"
|
|
32429
|
+
// for the current branch — distinct from the chip work in 0.52.0
|
|
32430
|
+
// which colours remote refs IN the row set. On a behind branch the
|
|
32431
|
+
// upstream commits aren't reachable from local HEAD, so the chips
|
|
32432
|
+
// alone can't signal "fetch / pull needed." This single line does.
|
|
32433
|
+
//
|
|
32434
|
+
// Two wording variants (behind-only vs diverged) live in the
|
|
32435
|
+
// helper; render is identical aside from the formatted string.
|
|
32436
|
+
// Warning yellow = same semantic as the remote-tracking chip kind.
|
|
32437
|
+
...((() => {
|
|
32438
|
+
const currentBranchRef = context.branches?.localBranches.find((branch) => branch.current);
|
|
32439
|
+
const banner = formatUpstreamAheadBanner(currentBranchRef, { ascii: theme.ascii });
|
|
32440
|
+
if (!banner)
|
|
32441
|
+
return [];
|
|
32442
|
+
return [h(Text, {
|
|
32443
|
+
key: 'upstream-ahead-banner',
|
|
32444
|
+
color: theme.noColor ? undefined : theme.colors.warning,
|
|
32445
|
+
}, banner)];
|
|
32446
|
+
})()),
|
|
32200
32447
|
// Server-side filter indicator (#776). Only rendered when the user
|
|
32201
32448
|
// has an active path:/author: prefix; clears when they Ctrl+U.
|
|
32202
32449
|
...(state.historyFetchArgs
|
|
@@ -36877,16 +37124,20 @@ function LogInkApp(deps) {
|
|
|
36877
37124
|
return;
|
|
36878
37125
|
}
|
|
36879
37126
|
// Success — close the overlay, reset compose (the staged set is
|
|
36880
|
-
// now empty since the plan committed everything), and
|
|
36881
|
-
//
|
|
36882
|
-
//
|
|
37127
|
+
// now empty since the plan committed everything), and route the
|
|
37128
|
+
// user to the history view so they see the just-landed commits
|
|
37129
|
+
// with the recent-commit marker firing on each row that was
|
|
37130
|
+
// created. Previous behavior popped compose to whatever was
|
|
37131
|
+
// beneath (often status — which now reads "clean worktree" and
|
|
37132
|
+
// gives the user no signal that anything just happened);
|
|
37133
|
+
// history is the natural follow-on surface.
|
|
37134
|
+
//
|
|
37135
|
+
// navigateHome nukes the rest of the stack so `<` after apply
|
|
37136
|
+
// doesn't walk back into the now-empty compose / status state
|
|
37137
|
+
// the user just left behind.
|
|
36883
37138
|
dispatch({ type: 'clearSplitPlan' });
|
|
36884
37139
|
dispatch({ type: 'commitCompose', action: { type: 'reset' } });
|
|
36885
|
-
|
|
36886
|
-
// invoked from a deeper stack and we don't want to over-pop.
|
|
36887
|
-
if (state.activeView === 'compose' && state.viewStack.length > 1) {
|
|
36888
|
-
dispatch({ type: 'popView' });
|
|
36889
|
-
}
|
|
37140
|
+
dispatch({ type: 'navigateHome' });
|
|
36890
37141
|
// Refresh BEFORE setting the final status so we can peek at the
|
|
36891
37142
|
// post-apply worktree state and craft a directive next-step hint
|
|
36892
37143
|
// ("X unstaged + Y untracked remaining — press gs to stage / I
|
|
@@ -36936,7 +37187,7 @@ function LogInkApp(deps) {
|
|
|
36936
37187
|
}
|
|
36937
37188
|
const successMessage = formatSplitApplySuccess(commitHashes.length, unstaged, untracked);
|
|
36938
37189
|
dispatch({ type: 'setStatus', value: successMessage, kind: 'success' });
|
|
36939
|
-
}, [dispatch, git, refreshContext, refreshHistoryRows, refreshWorktreeContext, state.
|
|
37190
|
+
}, [dispatch, git, refreshContext, refreshHistoryRows, refreshWorktreeContext, state.splitPlan]);
|
|
36940
37191
|
// Esc inside the overlay — close without applying. Status line gets
|
|
36941
37192
|
// a confirmation so the user knows the operation was abandoned.
|
|
36942
37193
|
const cancelCommitSplit = React.useCallback(() => {
|
|
@@ -37443,6 +37694,41 @@ function LogInkApp(deps) {
|
|
|
37443
37694
|
'fetch-remotes': async () => fetchRemotes(git),
|
|
37444
37695
|
'pull-current-branch': async () => pullCurrentBranch(git),
|
|
37445
37696
|
'push-current-branch': async () => pushCurrentBranch(git),
|
|
37697
|
+
// Per-branch fetch / pull / push that operate on the cursored
|
|
37698
|
+
// row in the branches sidebar. inkInput.ts dispatches these
|
|
37699
|
+
// when F / U / P fire from the sidebar; the *-current-branch
|
|
37700
|
+
// / fetch-remotes variants above still handle the same keys
|
|
37701
|
+
// from any other context.
|
|
37702
|
+
'fetch-selected-branch': async () => {
|
|
37703
|
+
const all = sortBranches(context.branches?.localBranches || [], state.branchSort);
|
|
37704
|
+
const visible = state.filter
|
|
37705
|
+
? all.filter((b) => matchesPromotedFilter([b.shortName, b.upstream || ''], state.filter))
|
|
37706
|
+
: all;
|
|
37707
|
+
const branch = visible[Math.min(state.selectedBranchIndex, visible.length - 1)];
|
|
37708
|
+
if (!branch)
|
|
37709
|
+
return { ok: false, message: 'No branch selected' };
|
|
37710
|
+
return fetchBranch(git, branch);
|
|
37711
|
+
},
|
|
37712
|
+
'pull-selected-branch': async () => {
|
|
37713
|
+
const all = sortBranches(context.branches?.localBranches || [], state.branchSort);
|
|
37714
|
+
const visible = state.filter
|
|
37715
|
+
? all.filter((b) => matchesPromotedFilter([b.shortName, b.upstream || ''], state.filter))
|
|
37716
|
+
: all;
|
|
37717
|
+
const branch = visible[Math.min(state.selectedBranchIndex, visible.length - 1)];
|
|
37718
|
+
if (!branch)
|
|
37719
|
+
return { ok: false, message: 'No branch selected' };
|
|
37720
|
+
return pullBranch(git, branch, context.branches?.currentBranch);
|
|
37721
|
+
},
|
|
37722
|
+
'push-selected-branch': async () => {
|
|
37723
|
+
const all = sortBranches(context.branches?.localBranches || [], state.branchSort);
|
|
37724
|
+
const visible = state.filter
|
|
37725
|
+
? all.filter((b) => matchesPromotedFilter([b.shortName, b.upstream || ''], state.filter))
|
|
37726
|
+
: all;
|
|
37727
|
+
const branch = visible[Math.min(state.selectedBranchIndex, visible.length - 1)];
|
|
37728
|
+
if (!branch)
|
|
37729
|
+
return { ok: false, message: 'No branch selected' };
|
|
37730
|
+
return pushBranch(git, branch);
|
|
37731
|
+
},
|
|
37446
37732
|
'rename-branch': async () => {
|
|
37447
37733
|
const newName = payload?.trim();
|
|
37448
37734
|
if (!newName)
|
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.53.0";
|
|
82
82
|
|
|
83
83
|
const isInteractive = (config) => {
|
|
84
84
|
return config?.mode === 'interactive' || !!config?.interactive;
|
|
@@ -15629,6 +15629,7 @@ Structural rules:
|
|
|
15629
15629
|
- Only use hunk IDs LITERALLY copied from the "Staged hunk inventory" section below. Do not invent or guess hunk IDs.
|
|
15630
15630
|
- If the hunk inventory says "No hunk-level inventory available" then EVERY group's "hunks" array MUST be empty (use only "files"). Do not write hunk IDs like "path::hunk-1" when no hunk inventory exists — those are not valid.
|
|
15631
15631
|
- Prefer 2-5 commits unless the changes are truly all one topic.
|
|
15632
|
+
- Order the groups in the sequence they would logically be built — foundational changes first, consumers after. If group B uses a symbol, function, type, or file introduced in group A, A MUST appear before B in the array. The applier commits in array order, so this order becomes the git history. Example: a "feat: add helpers" group that introduces \`formatX()\` must come before a "feat: wire helpers into renderer" group that calls \`formatX()\`, even if the staged diff is presented in the opposite order. When two groups have no dependency relationship, prefer the one closer to a "scaffold" (types, config, new files) before the one closer to a "use site" (existing files modified to consume the new code).
|
|
15632
15633
|
|
|
15633
15634
|
Commit message style:
|
|
15634
15635
|
- Write each "title" in the imperative mood ("add", not "added"), under 72 chars.
|
|
@@ -21085,6 +21086,37 @@ function getLogInkWorkflowActions() {
|
|
|
21085
21086
|
kind: 'normal',
|
|
21086
21087
|
requiresConfirmation: false,
|
|
21087
21088
|
},
|
|
21089
|
+
// Per-view variants of fetch / pull / push that act on the
|
|
21090
|
+
// cursored branch instead of the current one. Empty `key` keeps
|
|
21091
|
+
// them palette-discoverable without registering a global hotkey —
|
|
21092
|
+
// inkInput.ts dispatches them contextually when the user presses
|
|
21093
|
+
// F / U / P while the branches sidebar is focused. Outside that
|
|
21094
|
+
// context, the F / U / P keys still fire the global *-current-*
|
|
21095
|
+
// / fetch-remotes variants above.
|
|
21096
|
+
{
|
|
21097
|
+
id: 'fetch-selected-branch',
|
|
21098
|
+
key: '',
|
|
21099
|
+
label: 'Fetch selected branch',
|
|
21100
|
+
description: 'Run `git fetch <remote> <branch>` for the cursored branch in the branches view / sidebar.',
|
|
21101
|
+
kind: 'normal',
|
|
21102
|
+
requiresConfirmation: false,
|
|
21103
|
+
},
|
|
21104
|
+
{
|
|
21105
|
+
id: 'pull-selected-branch',
|
|
21106
|
+
key: '',
|
|
21107
|
+
label: 'Pull selected branch',
|
|
21108
|
+
description: 'Pull the cursored branch in the branches view / sidebar. Falls back to a fast-forward-only refspec fetch when the branch is not currently checked out; refuses non-FF.',
|
|
21109
|
+
kind: 'normal',
|
|
21110
|
+
requiresConfirmation: false,
|
|
21111
|
+
},
|
|
21112
|
+
{
|
|
21113
|
+
id: 'push-selected-branch',
|
|
21114
|
+
key: '',
|
|
21115
|
+
label: 'Push selected branch',
|
|
21116
|
+
description: 'Run `git push <remote> <branch>` for the cursored branch in the branches view / sidebar.',
|
|
21117
|
+
kind: 'normal',
|
|
21118
|
+
requiresConfirmation: false,
|
|
21119
|
+
},
|
|
21088
21120
|
{
|
|
21089
21121
|
// Per-view-only — the inkInput handler scopes this to the tags
|
|
21090
21122
|
// surface so we don't expose `R` as a remote-delete from elsewhere.
|
|
@@ -22025,8 +22057,22 @@ function getLogInkFooterHints(options) {
|
|
|
22025
22057
|
// "enter open" hint that drills into the dedicated view.
|
|
22026
22058
|
const itemsPresent = (options.sidebarItemCount ?? 0) > 0;
|
|
22027
22059
|
if (itemsPresent && options.sidebarTab === 'branches') {
|
|
22060
|
+
// P / U / F fire the global pull-current-branch, push-current-branch,
|
|
22061
|
+
// fetch-remotes workflows — already implemented, just not visible in
|
|
22062
|
+
// the footer before. Surfacing them here matters because the user's
|
|
22063
|
+
// attention is on a branch when the branches sidebar is focused;
|
|
22064
|
+
// pull / push / fetch are the next obvious actions.
|
|
22065
|
+
//
|
|
22066
|
+
// Note: `U` and `P` currently operate on the CURRENT branch, not the
|
|
22067
|
+
// cursored one. Task #5 will extend them to act on the cursored row;
|
|
22068
|
+
// until then the labels read as "current-branch ops" by virtue of
|
|
22069
|
+
// matching the workflow descriptions.
|
|
22028
22070
|
return {
|
|
22029
|
-
contextual: [
|
|
22071
|
+
contextual: [
|
|
22072
|
+
'↑/↓ branches', '←/→ tab', 'enter checkout',
|
|
22073
|
+
'F fetch', 'U pull', 'P push',
|
|
22074
|
+
'D delete', 'R rename', 'u upstream',
|
|
22075
|
+
],
|
|
22030
22076
|
global: NORMAL_GLOBAL_HINTS,
|
|
22031
22077
|
};
|
|
22032
22078
|
}
|
|
@@ -26455,6 +26501,27 @@ function getLogInkInputEvents(state, inputValue, key = {}, context = {}) {
|
|
|
26455
26501
|
events.push({ type: 'createManualCommit' });
|
|
26456
26502
|
return events;
|
|
26457
26503
|
}
|
|
26504
|
+
// Context-sensitive per-branch variants of F / U / P. When the
|
|
26505
|
+
// user has the branches sidebar / view focused with at least one
|
|
26506
|
+
// branch, F / U / P should act on the cursored row, not on the
|
|
26507
|
+
// current branch. This intercept fires BEFORE the generic
|
|
26508
|
+
// workflow-by-key lookup below so the global *-current-branch
|
|
26509
|
+
// variants don't shadow the contextual ones.
|
|
26510
|
+
//
|
|
26511
|
+
// Outside the branches context, the generic lookup runs and the
|
|
26512
|
+
// F / U / P keys hit the global `fetch-remotes` / `pull-current-branch`
|
|
26513
|
+
// / `push-current-branch` workflows as before.
|
|
26514
|
+
if (isBranchActionTarget(state) && context.branchCount) {
|
|
26515
|
+
if (inputValue === 'F') {
|
|
26516
|
+
return [{ type: 'runWorkflowAction', id: 'fetch-selected-branch' }];
|
|
26517
|
+
}
|
|
26518
|
+
if (inputValue === 'U') {
|
|
26519
|
+
return [{ type: 'runWorkflowAction', id: 'pull-selected-branch' }];
|
|
26520
|
+
}
|
|
26521
|
+
if (inputValue === 'P') {
|
|
26522
|
+
return [{ type: 'runWorkflowAction', id: 'push-selected-branch' }];
|
|
26523
|
+
}
|
|
26524
|
+
}
|
|
26458
26525
|
const workflowAction = getLogInkWorkflowActionByKey(inputValue);
|
|
26459
26526
|
if (workflowAction?.requiresConfirmation) {
|
|
26460
26527
|
return [action({ type: 'setPendingConfirmation', value: workflowAction.id })];
|
|
@@ -27557,6 +27624,106 @@ function pushCurrentBranch(git) {
|
|
|
27557
27624
|
function setUpstream(git, localBranch, upstreamBranch) {
|
|
27558
27625
|
return runAction$5(() => git.raw(['branch', '--set-upstream-to', upstreamBranch, localBranch]), `Set ${localBranch} upstream to ${upstreamBranch}`);
|
|
27559
27626
|
}
|
|
27627
|
+
/**
|
|
27628
|
+
* Push an arbitrary local branch (need not be the current branch) to
|
|
27629
|
+
* its remote. Refuses when the branch has no upstream and no remote
|
|
27630
|
+
* defaulting is configured — that branch needs a `git push -u …` from
|
|
27631
|
+
* the shell first.
|
|
27632
|
+
*
|
|
27633
|
+
* Pairs with `pushCurrentBranch` (no-arg variant); the workstation
|
|
27634
|
+
* dispatcher picks one or the other based on where the cursor is.
|
|
27635
|
+
*/
|
|
27636
|
+
function pushBranch(git, branch) {
|
|
27637
|
+
if (branch.type !== 'local') {
|
|
27638
|
+
return Promise.resolve({
|
|
27639
|
+
ok: false,
|
|
27640
|
+
message: 'Only local branches can be pushed.',
|
|
27641
|
+
});
|
|
27642
|
+
}
|
|
27643
|
+
if (!branch.upstream || !branch.remote) {
|
|
27644
|
+
return Promise.resolve({
|
|
27645
|
+
ok: false,
|
|
27646
|
+
message: `${branch.shortName} has no upstream — checkout the branch and run \`git push -u <remote> ${branch.shortName}\` first.`,
|
|
27647
|
+
});
|
|
27648
|
+
}
|
|
27649
|
+
return runAction$5(() => git.raw(['push', branch.remote, branch.shortName]), `Pushed ${branch.shortName} to ${branch.upstream}`);
|
|
27650
|
+
}
|
|
27651
|
+
/**
|
|
27652
|
+
* Fetch the cursored branch's upstream from its remote. Side-effect
|
|
27653
|
+
* free on the working tree — just updates the remote-tracking ref.
|
|
27654
|
+
* Works for any branch with an upstream regardless of checkout state.
|
|
27655
|
+
*
|
|
27656
|
+
* Falls back to a clean error when the branch has no upstream
|
|
27657
|
+
* configured (`git fetch <remote> <name>` would assume an unrelated
|
|
27658
|
+
* default refspec and surprise the user).
|
|
27659
|
+
*/
|
|
27660
|
+
function fetchBranch(git, branch) {
|
|
27661
|
+
if (branch.type !== 'local') {
|
|
27662
|
+
return Promise.resolve({
|
|
27663
|
+
ok: false,
|
|
27664
|
+
message: 'Only local branches can be fetched per-branch — use F to fetch all remotes.',
|
|
27665
|
+
});
|
|
27666
|
+
}
|
|
27667
|
+
if (!branch.upstream || !branch.remote) {
|
|
27668
|
+
return Promise.resolve({
|
|
27669
|
+
ok: false,
|
|
27670
|
+
message: `${branch.shortName} has no upstream — nothing to fetch.`,
|
|
27671
|
+
});
|
|
27672
|
+
}
|
|
27673
|
+
// `branch.upstream` is the short form (e.g. `origin/main`); the
|
|
27674
|
+
// ref name after the remote prefix is what fetch wants as the
|
|
27675
|
+
// refspec source. For a remote `origin` and upstream `origin/main`
|
|
27676
|
+
// we run `git fetch origin main`.
|
|
27677
|
+
const upstreamRef = branch.upstream.startsWith(`${branch.remote}/`)
|
|
27678
|
+
? branch.upstream.slice(branch.remote.length + 1)
|
|
27679
|
+
: branch.upstream;
|
|
27680
|
+
return runAction$5(() => git.raw(['fetch', branch.remote, upstreamRef]), `Fetched ${branch.upstream}`);
|
|
27681
|
+
}
|
|
27682
|
+
/**
|
|
27683
|
+
* Pull the cursored branch. Branches into two paths based on whether
|
|
27684
|
+
* the branch is currently checked out:
|
|
27685
|
+
*
|
|
27686
|
+
* - **Current branch**: defer to `pullCurrentBranch` (standard
|
|
27687
|
+
* `git pull --ff-only`).
|
|
27688
|
+
* - **Non-current branch**: use the refspec form
|
|
27689
|
+
* `git fetch <remote> <branch>:<branch>` which advances the local
|
|
27690
|
+
* ref to match the remote ref ONLY if the update is fast-forward.
|
|
27691
|
+
* Returns non-zero on non-FF without touching the working tree.
|
|
27692
|
+
* Diverged branches need a checkout + `pull --rebase` from the
|
|
27693
|
+
* user; we refuse rather than try to do that for them.
|
|
27694
|
+
*
|
|
27695
|
+
* `currentBranchName` lets the dispatcher compare without re-querying
|
|
27696
|
+
* git — it already has the value in `context.branches.currentBranch`.
|
|
27697
|
+
*/
|
|
27698
|
+
function pullBranch(git, branch, currentBranchName) {
|
|
27699
|
+
if (branch.type !== 'local') {
|
|
27700
|
+
return Promise.resolve({
|
|
27701
|
+
ok: false,
|
|
27702
|
+
message: 'Only local branches can be pulled.',
|
|
27703
|
+
});
|
|
27704
|
+
}
|
|
27705
|
+
if (!branch.upstream || !branch.remote) {
|
|
27706
|
+
return Promise.resolve({
|
|
27707
|
+
ok: false,
|
|
27708
|
+
message: `${branch.shortName} has no upstream — nothing to pull.`,
|
|
27709
|
+
});
|
|
27710
|
+
}
|
|
27711
|
+
// Current branch — defer to the in-place workflow.
|
|
27712
|
+
if (branch.shortName === currentBranchName) {
|
|
27713
|
+
return pullCurrentBranch(git);
|
|
27714
|
+
}
|
|
27715
|
+
// Non-current branch — refspec-based fast-forward refusing non-FF.
|
|
27716
|
+
// `branch.upstream` is `<remote>/<ref>`; strip the remote prefix to
|
|
27717
|
+
// get the upstream ref name to fetch.
|
|
27718
|
+
const upstreamRef = branch.upstream.startsWith(`${branch.remote}/`)
|
|
27719
|
+
? branch.upstream.slice(branch.remote.length + 1)
|
|
27720
|
+
: branch.upstream;
|
|
27721
|
+
return runAction$5(() => git.raw([
|
|
27722
|
+
'fetch',
|
|
27723
|
+
branch.remote,
|
|
27724
|
+
`${upstreamRef}:${branch.shortName}`,
|
|
27725
|
+
]), `Fast-forwarded ${branch.shortName} to ${branch.upstream}`);
|
|
27726
|
+
}
|
|
27560
27727
|
|
|
27561
27728
|
async function runAction$4(action, successMessage) {
|
|
27562
27729
|
try {
|
|
@@ -29080,29 +29247,81 @@ function formatBranchDivergence(branch, options = {}) {
|
|
|
29080
29247
|
parts.push(`↓${branch.behind}`);
|
|
29081
29248
|
return `${parts.join(' ')} ${branch.upstream}`;
|
|
29082
29249
|
}
|
|
29083
|
-
|
|
29084
|
-
|
|
29085
|
-
|
|
29086
|
-
|
|
29087
|
-
|
|
29088
|
-
|
|
29089
|
-
|
|
29090
|
-
|
|
29091
|
-
|
|
29092
|
-
|
|
29093
|
-
|
|
29094
|
-
|
|
29250
|
+
function formatUpstreamAheadBanner(branch, options = {}) {
|
|
29251
|
+
if (!branch?.upstream || branch.behind <= 0) {
|
|
29252
|
+
return undefined;
|
|
29253
|
+
}
|
|
29254
|
+
const sep = options.ascii ? '.' : '·';
|
|
29255
|
+
if (branch.ahead > 0) {
|
|
29256
|
+
// Diverged — local has work too, fast-forward pull is impossible.
|
|
29257
|
+
// Suggest pull --rebase as the cleaner-history default; users who
|
|
29258
|
+
// prefer merge can do that themselves.
|
|
29259
|
+
const symbols = options.ascii
|
|
29260
|
+
? `+${branch.ahead} -${branch.behind}`
|
|
29261
|
+
: `↑${branch.ahead} ↓${branch.behind}`;
|
|
29262
|
+
return `${symbols} diverged from ${branch.upstream} ${sep} F fetch ${sep} U pull --rebase`;
|
|
29263
|
+
}
|
|
29264
|
+
// Behind-only — fast-forward pull works.
|
|
29265
|
+
const arrow = options.ascii ? 'v' : '↓';
|
|
29266
|
+
const noun = branch.behind === 1 ? 'commit' : 'commits';
|
|
29267
|
+
return `${arrow} ${branch.behind} ${noun} behind ${branch.upstream} ${sep} F fetch ${sep} U pull`;
|
|
29268
|
+
}
|
|
29095
29269
|
function branchRowMarker(branch, options = {}) {
|
|
29096
|
-
if (branch.current)
|
|
29097
|
-
return '*';
|
|
29098
|
-
|
|
29099
|
-
|
|
29270
|
+
if (branch.current) {
|
|
29271
|
+
return { glyph: '*', kind: 'head' };
|
|
29272
|
+
}
|
|
29273
|
+
if (!branch.upstream) {
|
|
29274
|
+
return { glyph: options.ascii ? '?' : '◌', kind: 'no-upstream' };
|
|
29275
|
+
}
|
|
29100
29276
|
const ahead = branch.ahead ?? 0;
|
|
29101
29277
|
const behind = branch.behind ?? 0;
|
|
29102
29278
|
if (ahead === 0 && behind === 0) {
|
|
29103
|
-
return options.ascii ? '=' : '≡';
|
|
29279
|
+
return { glyph: options.ascii ? '=' : '≡', kind: 'synced' };
|
|
29280
|
+
}
|
|
29281
|
+
if (ahead > 0 && behind > 0) {
|
|
29282
|
+
return { glyph: options.ascii ? '~' : '⇅', kind: 'diverged' };
|
|
29283
|
+
}
|
|
29284
|
+
if (behind > 0) {
|
|
29285
|
+
return { glyph: options.ascii ? 'v' : '↓', kind: 'behind' };
|
|
29286
|
+
}
|
|
29287
|
+
// ahead > 0 (the only remaining case after the guards above)
|
|
29288
|
+
return { glyph: options.ascii ? '^' : '↑', kind: 'ahead' };
|
|
29289
|
+
}
|
|
29290
|
+
/**
|
|
29291
|
+
* Theme-aware colour picker for a `BranchRowMarker.kind`.
|
|
29292
|
+
*
|
|
29293
|
+
* Reuses the existing chip / banner colour semantic so the workstation
|
|
29294
|
+
* speaks one visual language across history (chips, "behind upstream"
|
|
29295
|
+
* banner) and the branches list:
|
|
29296
|
+
*
|
|
29297
|
+
* - `head` → success green (matches HEAD chip)
|
|
29298
|
+
* - `behind` → warning yellow (matches "behind upstream" banner)
|
|
29299
|
+
* - `diverged` → warning yellow (same: action needed inbound)
|
|
29300
|
+
* - `ahead` → info blue (you have work to push)
|
|
29301
|
+
* - `synced` → undefined (neutral; inherit row's existing dim)
|
|
29302
|
+
* - `no-upstream` → undefined (neutral; same)
|
|
29303
|
+
*
|
|
29304
|
+
* Returns `undefined` under `noColor` / `ascii` for the muted cases so
|
|
29305
|
+
* the row renderer skips the colour wrap entirely; the glyph alone
|
|
29306
|
+
* carries the meaning.
|
|
29307
|
+
*/
|
|
29308
|
+
function getBranchRowMarkerColor(kind, theme) {
|
|
29309
|
+
if (theme.noColor)
|
|
29310
|
+
return undefined;
|
|
29311
|
+
switch (kind) {
|
|
29312
|
+
case 'head':
|
|
29313
|
+
return theme.colors.success;
|
|
29314
|
+
case 'behind':
|
|
29315
|
+
case 'diverged':
|
|
29316
|
+
return theme.colors.warning;
|
|
29317
|
+
case 'ahead':
|
|
29318
|
+
return theme.colors.info;
|
|
29319
|
+
case 'synced':
|
|
29320
|
+
case 'no-upstream':
|
|
29321
|
+
return undefined;
|
|
29322
|
+
default:
|
|
29323
|
+
return undefined;
|
|
29104
29324
|
}
|
|
29105
|
-
return options.ascii ? '~' : '↕';
|
|
29106
29325
|
}
|
|
29107
29326
|
/**
|
|
29108
29327
|
* Compact, human-friendly relative timestamp for the branch row.
|
|
@@ -29710,7 +29929,7 @@ function renderActiveSidebarContent(h, Text, tab, state, context, contextStatus,
|
|
|
29710
29929
|
];
|
|
29711
29930
|
return [
|
|
29712
29931
|
...headerRows,
|
|
29713
|
-
...renderSelectableSidebarRows(h, Text, sortedBranches, state.selectedBranchIndex, focused, width, theme, (branch) => `${branchRowMarker(branch, { ascii: theme.ascii })} ${branch.shortName}`, 'tab-branches', visibleListCount),
|
|
29932
|
+
...renderSelectableSidebarRows(h, Text, sortedBranches, state.selectedBranchIndex, focused, width, theme, (branch) => `${branchRowMarker(branch, { ascii: theme.ascii }).glyph} ${branch.shortName}`, 'tab-branches', visibleListCount),
|
|
29714
29933
|
];
|
|
29715
29934
|
}
|
|
29716
29935
|
if (tab === 'tags') {
|
|
@@ -30173,21 +30392,22 @@ function renderBranchesSurface(h, components, state, context, contextStatus, bod
|
|
|
30173
30392
|
const isSelected = index === selected;
|
|
30174
30393
|
const cursor = isSelected ? '>' : ' ';
|
|
30175
30394
|
const marker = branchRowMarker(branch, { ascii: theme.ascii });
|
|
30395
|
+
const markerColor = getBranchRowMarkerColor(marker.kind, theme);
|
|
30176
30396
|
const divergence = formatBranchDivergence(branch, { ascii: theme.ascii });
|
|
30177
30397
|
const lastTouched = formatBranchLastTouched(branch.date, new Date());
|
|
30178
30398
|
// Split the row into spans so the timestamp stays dim even on the
|
|
30179
|
-
// currently-selected (bold) row
|
|
30180
|
-
//
|
|
30181
|
-
// right-padded so the divergence column stays aligned across rows.
|
|
30399
|
+
// currently-selected (bold) row, and the sync-state marker keeps
|
|
30400
|
+
// its own colour even when the surrounding row text is dimmed.
|
|
30182
30401
|
const namePadded = truncateCells(branch.shortName, nameColWidth).padEnd(nameColWidth);
|
|
30183
30402
|
const timestampPadded = lastTouched.padEnd(8);
|
|
30184
30403
|
const lineDim = !isSelected && !branch.current;
|
|
30185
|
-
const
|
|
30404
|
+
const cursorAndPad = `${cursor} `;
|
|
30405
|
+
const trailingName = ` ${namePadded} `;
|
|
30186
30406
|
const trailingDivergence = divergence ? ` ${divergence}` : '';
|
|
30187
30407
|
// Truncate the assembled line to the actual panel width so a
|
|
30188
30408
|
// narrow inspector / sidebar focus doesn't push branch rows
|
|
30189
30409
|
// onto a second visual line (#830).
|
|
30190
|
-
const fullText = `${
|
|
30410
|
+
const fullText = `${cursorAndPad}${marker.glyph}${trailingName}${timestampPadded}${trailingDivergence}`;
|
|
30191
30411
|
const truncated = truncateCells(fullText, Math.max(20, width - 4));
|
|
30192
30412
|
// If truncation chopped into the timestamp/divergence portion,
|
|
30193
30413
|
// fall back to a single Text to keep the visible width honest.
|
|
@@ -30202,7 +30422,15 @@ function renderBranchesSurface(h, components, state, context, contextStatus, bod
|
|
|
30202
30422
|
key: `branch-${index}`,
|
|
30203
30423
|
bold: isSelected,
|
|
30204
30424
|
dimColor: lineDim,
|
|
30205
|
-
},
|
|
30425
|
+
}, cursorAndPad,
|
|
30426
|
+
// The marker carries the sync-state colour; an explicit
|
|
30427
|
+
// `dimColor: false` on this span keeps the colour bright even
|
|
30428
|
+
// when the surrounding row is dim (other branches in the list
|
|
30429
|
+
// dim out under the existing `lineDim` rule). The synced /
|
|
30430
|
+
// no-upstream kinds return undefined from
|
|
30431
|
+
// `getBranchRowMarkerColor`, so those markers inherit the
|
|
30432
|
+
// row's dim and read as quiet chrome.
|
|
30433
|
+
h(Text, { color: markerColor, dimColor: markerColor ? false : undefined }, marker.glyph), trailingName, h(Text, { dimColor: true }, timestampPadded), trailingDivergence);
|
|
30206
30434
|
});
|
|
30207
30435
|
return h(Box, {
|
|
30208
30436
|
borderColor: focusBorderColor(theme, focused),
|
|
@@ -32214,6 +32442,25 @@ function renderHistoryPanel(h, components, state, context, bodyRows, width, them
|
|
|
32214
32442
|
paddingX: 1,
|
|
32215
32443
|
width,
|
|
32216
32444
|
}, h(Box, { justifyContent: 'space-between' }, h(Text, { bold: true }, panelTitle('Commits', focused)), h(Text, { dimColor: true }, `${title} | ${graphMode} | ${loadState}`)),
|
|
32445
|
+
// Upstream-ahead banner. Surfaces "the remote has work you don't"
|
|
32446
|
+
// for the current branch — distinct from the chip work in 0.52.0
|
|
32447
|
+
// which colours remote refs IN the row set. On a behind branch the
|
|
32448
|
+
// upstream commits aren't reachable from local HEAD, so the chips
|
|
32449
|
+
// alone can't signal "fetch / pull needed." This single line does.
|
|
32450
|
+
//
|
|
32451
|
+
// Two wording variants (behind-only vs diverged) live in the
|
|
32452
|
+
// helper; render is identical aside from the formatted string.
|
|
32453
|
+
// Warning yellow = same semantic as the remote-tracking chip kind.
|
|
32454
|
+
...((() => {
|
|
32455
|
+
const currentBranchRef = context.branches?.localBranches.find((branch) => branch.current);
|
|
32456
|
+
const banner = formatUpstreamAheadBanner(currentBranchRef, { ascii: theme.ascii });
|
|
32457
|
+
if (!banner)
|
|
32458
|
+
return [];
|
|
32459
|
+
return [h(Text, {
|
|
32460
|
+
key: 'upstream-ahead-banner',
|
|
32461
|
+
color: theme.noColor ? undefined : theme.colors.warning,
|
|
32462
|
+
}, banner)];
|
|
32463
|
+
})()),
|
|
32217
32464
|
// Server-side filter indicator (#776). Only rendered when the user
|
|
32218
32465
|
// has an active path:/author: prefix; clears when they Ctrl+U.
|
|
32219
32466
|
...(state.historyFetchArgs
|
|
@@ -36894,16 +37141,20 @@ function LogInkApp(deps) {
|
|
|
36894
37141
|
return;
|
|
36895
37142
|
}
|
|
36896
37143
|
// Success — close the overlay, reset compose (the staged set is
|
|
36897
|
-
// now empty since the plan committed everything), and
|
|
36898
|
-
//
|
|
36899
|
-
//
|
|
37144
|
+
// now empty since the plan committed everything), and route the
|
|
37145
|
+
// user to the history view so they see the just-landed commits
|
|
37146
|
+
// with the recent-commit marker firing on each row that was
|
|
37147
|
+
// created. Previous behavior popped compose to whatever was
|
|
37148
|
+
// beneath (often status — which now reads "clean worktree" and
|
|
37149
|
+
// gives the user no signal that anything just happened);
|
|
37150
|
+
// history is the natural follow-on surface.
|
|
37151
|
+
//
|
|
37152
|
+
// navigateHome nukes the rest of the stack so `<` after apply
|
|
37153
|
+
// doesn't walk back into the now-empty compose / status state
|
|
37154
|
+
// the user just left behind.
|
|
36900
37155
|
dispatch({ type: 'clearSplitPlan' });
|
|
36901
37156
|
dispatch({ type: 'commitCompose', action: { type: 'reset' } });
|
|
36902
|
-
|
|
36903
|
-
// invoked from a deeper stack and we don't want to over-pop.
|
|
36904
|
-
if (state.activeView === 'compose' && state.viewStack.length > 1) {
|
|
36905
|
-
dispatch({ type: 'popView' });
|
|
36906
|
-
}
|
|
37157
|
+
dispatch({ type: 'navigateHome' });
|
|
36907
37158
|
// Refresh BEFORE setting the final status so we can peek at the
|
|
36908
37159
|
// post-apply worktree state and craft a directive next-step hint
|
|
36909
37160
|
// ("X unstaged + Y untracked remaining — press gs to stage / I
|
|
@@ -36953,7 +37204,7 @@ function LogInkApp(deps) {
|
|
|
36953
37204
|
}
|
|
36954
37205
|
const successMessage = formatSplitApplySuccess(commitHashes.length, unstaged, untracked);
|
|
36955
37206
|
dispatch({ type: 'setStatus', value: successMessage, kind: 'success' });
|
|
36956
|
-
}, [dispatch, git, refreshContext, refreshHistoryRows, refreshWorktreeContext, state.
|
|
37207
|
+
}, [dispatch, git, refreshContext, refreshHistoryRows, refreshWorktreeContext, state.splitPlan]);
|
|
36957
37208
|
// Esc inside the overlay — close without applying. Status line gets
|
|
36958
37209
|
// a confirmation so the user knows the operation was abandoned.
|
|
36959
37210
|
const cancelCommitSplit = React.useCallback(() => {
|
|
@@ -37460,6 +37711,41 @@ function LogInkApp(deps) {
|
|
|
37460
37711
|
'fetch-remotes': async () => fetchRemotes(git),
|
|
37461
37712
|
'pull-current-branch': async () => pullCurrentBranch(git),
|
|
37462
37713
|
'push-current-branch': async () => pushCurrentBranch(git),
|
|
37714
|
+
// Per-branch fetch / pull / push that operate on the cursored
|
|
37715
|
+
// row in the branches sidebar. inkInput.ts dispatches these
|
|
37716
|
+
// when F / U / P fire from the sidebar; the *-current-branch
|
|
37717
|
+
// / fetch-remotes variants above still handle the same keys
|
|
37718
|
+
// from any other context.
|
|
37719
|
+
'fetch-selected-branch': async () => {
|
|
37720
|
+
const all = sortBranches(context.branches?.localBranches || [], state.branchSort);
|
|
37721
|
+
const visible = state.filter
|
|
37722
|
+
? all.filter((b) => matchesPromotedFilter([b.shortName, b.upstream || ''], state.filter))
|
|
37723
|
+
: all;
|
|
37724
|
+
const branch = visible[Math.min(state.selectedBranchIndex, visible.length - 1)];
|
|
37725
|
+
if (!branch)
|
|
37726
|
+
return { ok: false, message: 'No branch selected' };
|
|
37727
|
+
return fetchBranch(git, branch);
|
|
37728
|
+
},
|
|
37729
|
+
'pull-selected-branch': async () => {
|
|
37730
|
+
const all = sortBranches(context.branches?.localBranches || [], state.branchSort);
|
|
37731
|
+
const visible = state.filter
|
|
37732
|
+
? all.filter((b) => matchesPromotedFilter([b.shortName, b.upstream || ''], state.filter))
|
|
37733
|
+
: all;
|
|
37734
|
+
const branch = visible[Math.min(state.selectedBranchIndex, visible.length - 1)];
|
|
37735
|
+
if (!branch)
|
|
37736
|
+
return { ok: false, message: 'No branch selected' };
|
|
37737
|
+
return pullBranch(git, branch, context.branches?.currentBranch);
|
|
37738
|
+
},
|
|
37739
|
+
'push-selected-branch': async () => {
|
|
37740
|
+
const all = sortBranches(context.branches?.localBranches || [], state.branchSort);
|
|
37741
|
+
const visible = state.filter
|
|
37742
|
+
? all.filter((b) => matchesPromotedFilter([b.shortName, b.upstream || ''], state.filter))
|
|
37743
|
+
: all;
|
|
37744
|
+
const branch = visible[Math.min(state.selectedBranchIndex, visible.length - 1)];
|
|
37745
|
+
if (!branch)
|
|
37746
|
+
return { ok: false, message: 'No branch selected' };
|
|
37747
|
+
return pushBranch(git, branch);
|
|
37748
|
+
},
|
|
37463
37749
|
'rename-branch': async () => {
|
|
37464
37750
|
const newName = payload?.trim();
|
|
37465
37751
|
if (!newName)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "git-coco",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.53.0",
|
|
4
4
|
"description": "zero-effort git commits with coco.",
|
|
5
5
|
"author": "gfargo <ghfargo@gmail.com>",
|
|
6
6
|
"license": "MIT",
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
"test:publish": "npm run lint && npm run build && npm run test:cli && npm pack --dry-run",
|
|
42
42
|
"test:cli": "tsx bin/smokeCli.ts",
|
|
43
43
|
"bench": "tsx bin/benchmark.ts",
|
|
44
|
-
"scenario": "
|
|
44
|
+
"scenario": "tsx bin/scenarioRunner.ts",
|
|
45
45
|
"eval:structural-extract": "tsx bin/structuralExtractEval.ts",
|
|
46
46
|
"pretest:jest": "npm run build:info && node bin/copyTreeSitterWasm.mjs",
|
|
47
47
|
"test:jest": "cross-env NODE_OPTIONS=--experimental-vm-modules jest",
|
|
@@ -61,7 +61,7 @@
|
|
|
61
61
|
},
|
|
62
62
|
"devDependencies": {
|
|
63
63
|
"@commitlint/types": "^20.5.0",
|
|
64
|
-
"@gfargo/git-scenarios": "^0.3.
|
|
64
|
+
"@gfargo/git-scenarios": "^0.3.3",
|
|
65
65
|
"@rollup/plugin-commonjs": "^29.0.0",
|
|
66
66
|
"@rollup/plugin-eslint": "^9.0.5",
|
|
67
67
|
"@rollup/plugin-json": "^6.0.0",
|