dsh-ide-git 0.19.0 → 0.19.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/dsh.plugin.json +1 -1
- package/package.json +1 -1
- package/src/client.js +172 -45
- package/src/index.js +45 -1
package/dsh.plugin.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "dsh-external/dsh-ide-git",
|
|
3
|
-
"version": "0.19.
|
|
3
|
+
"version": "0.19.2",
|
|
4
4
|
"main": "./src/index.js",
|
|
5
5
|
"description": "IDE 级 Git 操作面板:分支树 + 提交图谱 + 变更/暂存/提交 + 提交详情。装了 dsh-better-sidebar 时挂成它的原生 tab(右侧栏 + 底部工作台);没装时直接挂 DSH 原生右侧栏,单独安装即可用。JetBrains 风格 Git 工具窗口 for DeepSeek Harness.",
|
|
6
6
|
"engines": {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-ide-git",
|
|
3
|
-
"version": "0.19.
|
|
3
|
+
"version": "0.19.2",
|
|
4
4
|
"description": "DSH plugin: an IDE-grade Git tool window as a native dsh-better-sidebar tab — branch tree, commit graph, changes, commit details, JetBrains-style actions, in the right sidebar and the bottom panel. | DSH 插件:IDE 级 Git 工具窗口,以 dsh-better-sidebar 原生 Tab 挂载——分支树 / 提交图谱 / 变更与提交详情 / JetBrains 风格操作,右侧栏与底部面板双布局自适应。",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|
package/src/client.js
CHANGED
|
@@ -11035,7 +11035,7 @@ window.__ModuleLoader__.load({
|
|
|
11035
11035
|
}, E(Icon, { name: 'more', size: 18 })))
|
|
11036
11036
|
/* The selection action bar: visible only while a multi-select exists, so
|
|
11037
11037
|
the mouse-only path (no right-click, no keyboard) keeps full power. */
|
|
11038
|
-
const selectionBar = selection.size === 0 ? null : E('div', { className: 'dig-selection-bar' },
|
|
11038
|
+
const selectionBar = selection.size === 0 ? null : E('div', { className: 'dig-selection-bar', 'data-cordis-panel': '' },
|
|
11039
11039
|
E('span', { className: 'dig-selection-count' }, fill(t('selection.count'), { n: selection.size })),
|
|
11040
11040
|
props.onBulkStage === undefined ? null : E('button', { type: 'button', className: 'dig-link', onClick: () => props.onBulkStage() }, t('action.stage')),
|
|
11041
11041
|
props.onBulkUnstage === undefined ? null : E('button', { type: 'button', className: 'dig-link', onClick: () => props.onBulkUnstage() }, t('action.unstage')),
|
|
@@ -11068,8 +11068,12 @@ window.__ModuleLoader__.load({
|
|
|
11068
11068
|
visibleItems.map((item) => E(Fragment, { key: item.key }, item.node)),
|
|
11069
11069
|
winLast < changeItems.length ? E('div', { key: 'pad-bottom', style: { height: String(changeOffsets[changeItems.length] - changeOffsets[winLast]) + 'px' } }) : null,
|
|
11070
11070
|
]),
|
|
11071
|
+
/* In-flow BETWEEN the list and the composer: a first selection
|
|
11072
|
+
shrinks the viewport bottom edge without moving the list content
|
|
11073
|
+
(scrollTop untouched), so the v0.18.3 dblclick precision holds —
|
|
11074
|
+
and nothing on the list (group head, rows) is ever covered. */
|
|
11075
|
+
selectionBar,
|
|
11071
11076
|
composer),
|
|
11072
|
-
selectionBar,
|
|
11073
11077
|
marquee === null ? null : E('div', {
|
|
11074
11078
|
className: 'dig-marquee',
|
|
11075
11079
|
style: { left: marquee.x + 'px', top: marquee.y + 'px', width: marquee.w + 'px', height: marquee.h + 'px' },
|
|
@@ -12021,9 +12025,14 @@ window.__ModuleLoader__.load({
|
|
|
12021
12025
|
roll live state back to a stale snapshot. */
|
|
12022
12026
|
const cachedPage = readPaintPage(repoRoot)
|
|
12023
12027
|
if (cachedPage !== undefined) {
|
|
12024
|
-
|
|
12025
|
-
|
|
12026
|
-
|
|
12028
|
+
/* Field-wise: a pill may have painted only its summary (the pill
|
|
12029
|
+
merges its own two fields alone), so branches/commits can be
|
|
12030
|
+
absent from the page. Seeding undefined into state would crash
|
|
12031
|
+
the branch-tree render (undefined.remotes) — skip that field
|
|
12032
|
+
and let refresh() fetch it as usual. */
|
|
12033
|
+
if (summary === null && cachedPage.summary !== undefined) setSummary(cachedPage.summary)
|
|
12034
|
+
if (branches === null && cachedPage.branches !== undefined) setBranches(cachedPage.branches)
|
|
12035
|
+
if (commits.length === 0 && Array.isArray(cachedPage.commits)) { setCommits(cachedPage.commits); setHasMore(cachedPage.hasMore === true) }
|
|
12027
12036
|
}
|
|
12028
12037
|
void refresh()
|
|
12029
12038
|
return undefined
|
|
@@ -13005,7 +13014,7 @@ window.__ModuleLoader__.load({
|
|
|
13005
13014
|
}
|
|
13006
13015
|
|
|
13007
13016
|
/* Remotes themselves, for "pull from" / "push to" style entries. */
|
|
13008
|
-
const remoteNames = branches
|
|
13017
|
+
const remoteNames = branches == null ? [] : branches.remotes.map((entry) => entry.name === undefined ? String(entry) : entry.name)
|
|
13009
13018
|
|
|
13010
13019
|
const showWorkingDiff = () => {
|
|
13011
13020
|
const changes = summary === null ? null : summary.changes
|
|
@@ -14123,12 +14132,26 @@ window.__ModuleLoader__.load({
|
|
|
14123
14132
|
VIEWPORT-space .dig-changes (invariant 13: position:fixed would be
|
|
14124
14133
|
captured by the workbench's contain:layout and flung off-panel). */
|
|
14125
14134
|
'.dig-marquee{position:absolute;border:1px solid var(--dsw-alias-brand-primary);background:color-mix(in srgb,var(--dsw-alias-brand-primary) 14%,transparent);pointer-events:none;z-index:6}',
|
|
14126
|
-
/* The selection bar
|
|
14127
|
-
|
|
14128
|
-
the first
|
|
14129
|
-
|
|
14130
|
-
|
|
14131
|
-
|
|
14135
|
+
/* The selection action bar (v0.19.1 redesign). It used to FLOAT over the
|
|
14136
|
+
top of the list (absolute, v0.18.3): that covered the group head and
|
|
14137
|
+
the first rows, and on transparency skins (dsh-any-background) its
|
|
14138
|
+
background went see-through, so its own text blended into the rows
|
|
14139
|
+
underneath — unreadable. Now it is an IN-FLOW island between the list
|
|
14140
|
+
and the composer: nothing is covered, and a first selection only
|
|
14141
|
+
shrinks the viewport bottom edge (scrollTop untouched), which keeps
|
|
14142
|
+
the v0.18.3 dblclick precision. The surface rides the official menu
|
|
14143
|
+
token; data-cordis-panel (set at the render site) lets transparency
|
|
14144
|
+
skins sync it exactly like the official menus. */
|
|
14145
|
+
/* Local alias restore: dsh-any-background re-declares --dsw-alias-bg-layer-1/2/3
|
|
14146
|
+
on [data-conversation-composer-overlay] (the session seat) as its trajectory
|
|
14147
|
+
vars, which are absent when that feature is off — the seat inherits
|
|
14148
|
+
guaranteed-invalid values, and var() fallbacks do NOT rescue IVCT, so the
|
|
14149
|
+
whole background chain below resolves to transparent. Re-declaring the two
|
|
14150
|
+
aliases from the official static token (the exact source the light theme's
|
|
14151
|
+
own alias defs point at, theme-correct in dark too) makes the chain valid
|
|
14152
|
+
again regardless of any-bg's injection state. */
|
|
14153
|
+
'.dig-selection-bar{--dsw-alias-bg-layer-1:var(--dsw-static-neutral-bluish-00,#fff);--dsw-alias-bg-layer-2:var(--dsw-static-neutral-bluish-00,#fff);display:flex;align-items:center;gap:10px;flex-wrap:wrap;padding:5px 10px;margin:6px 0;font-size:11px;line-height:16px;border-radius:var(--dsw-radius-sm,8px);background:var(--dsh-any-op-menu-cordis,var(--dsw-alias-bg-layer-2));border:1px solid var(--dsw-alias-border-l2,var(--dsw-alias-border-subtle,transparent));box-shadow:var(--dsw-elevation-prominent,0 4px 14px rgba(0,0,0,.25))}',
|
|
14154
|
+
'.dig-selection-bar .dig-link{margin-left:0;padding:2px 0}',
|
|
14132
14155
|
'.dig-selection-bar .dig-link-danger{color:var(--dsw-alias-state-error-primary)}',
|
|
14133
14156
|
'.dig-selection-count{color:var(--dsw-alias-label-secondary);white-space:nowrap}',
|
|
14134
14157
|
/* Focusable lists keep their own selection highlight as the focus
|
|
@@ -14333,14 +14356,24 @@ window.__ModuleLoader__.load({
|
|
|
14333
14356
|
'.dig-cs-behind{color:var(--dsw-alias-state-business-primary)}',
|
|
14334
14357
|
'.dig-cs-ahead{color:var(--dsw-alias-state-success-primary)}',
|
|
14335
14358
|
'.dig-cs-error{display:inline-flex;align-items:center;color:var(--dsw-alias-state-error-primary);font-size:12px;padding:0 3px;cursor:help}',
|
|
14336
|
-
|
|
14359
|
+
// .dig-cs-menu opens upward (the composer sits at the bottom of the UI). The surface
|
|
14360
|
+
// carries no backdrop-filter of its own: the data-cordis-panel attribute opts the popover
|
|
14361
|
+
// into dsh-any-background's input-surface channel (--dsw-specific-menu remapped to
|
|
14362
|
+
// var(--dsh-any-op-menu-cordis) on body — see the .dig-selection-bar note:
|
|
14363
|
+
// leading with the op var keeps the fill valid even when the plugin's
|
|
14364
|
+
// surface sampling misses and that remap would otherwise IVCT to transparent).
|
|
14365
|
+
// --dsh-any-op-menu-cordis at the input opacity tier, plus its input blur rule), so fill
|
|
14366
|
+
// opacity and frost follow the composer card exactly. Without that plugin the
|
|
14367
|
+
// theme-owned menu material applies.
|
|
14368
|
+
'.dig-cs-menu{--dsw-alias-bg-layer-1:var(--dsw-static-neutral-bluish-00,#fff);--dsw-alias-bg-layer-2:var(--dsw-static-neutral-bluish-00,#fff);position:absolute;bottom:calc(100% + 6px);left:0;z-index:60;min-width:220px;max-width:320px;max-height:320px;overflow:auto;padding:6px;border-radius:var(--dsw-radius-lg,12px);background:var(--dsh-any-op-menu-cordis,var(--dsw-alias-bg-layer-2));box-shadow:var(--dsw-elevation-prominent,var(--dsw-elevation-dimmed,0 8px 24px rgba(0,0,0,.2)));color:var(--dsw-alias-label-secondary);font-size:12px;line-height:20px}',
|
|
14337
14369
|
'.dig-cs-item{display:flex;align-items:center;gap:8px;width:100%;padding:5px 10px;border:none;border-radius:var(--dsw-radius-sm,8px);background:transparent;color:var(--dsw-alias-label-primary);font-family:inherit;font-size:13px;line-height:20px;cursor:pointer;text-align:left}',
|
|
14338
14370
|
'.dig-cs-item:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover)}',
|
|
14339
14371
|
'.dig-cs-item:disabled{opacity:.5;cursor:default}',
|
|
14340
14372
|
'.dig-cs-item[data-current=true]{color:var(--dsw-alias-brand-primary)}',
|
|
14341
14373
|
'.dig-cs-item svg{flex:none}',
|
|
14342
14374
|
'.dig-cs-item-name{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}',
|
|
14343
|
-
'.dig-cs-item-
|
|
14375
|
+
'.dig-cs-item-main{display:flex;flex-direction:column;min-width:0;flex:1}',
|
|
14376
|
+
'.dig-cs-item-sub{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--dsw-alias-label-tertiary);font-size:10px;line-height:12px}',
|
|
14344
14377
|
'.dig-cs-item-note{color:var(--dsw-alias-label-tertiary);font-size:11px;flex:none}',
|
|
14345
14378
|
'.dig-cs-item-counts{display:inline-flex;gap:5px;margin-left:auto;flex:none;font-variant-numeric:tabular-nums;font-size:11px}',
|
|
14346
14379
|
'.dig-cs-static{color:var(--dsw-alias-label-tertiary);cursor:default}',
|
|
@@ -14480,14 +14513,68 @@ window.__ModuleLoader__.load({
|
|
|
14480
14513
|
const COMPOSER_POLL_MS = 30000
|
|
14481
14514
|
const COMPOSER_ERROR_TTL_MS = 6000
|
|
14482
14515
|
|
|
14516
|
+
/* Windows paths ride backslashes, so both pill labels cut the leaf with
|
|
14517
|
+
this slash-aware split instead of a bare forward-slash split — a drive
|
|
14518
|
+
path must read as the leaf name, not the whole path. */
|
|
14519
|
+
function baseNameOf(path) {
|
|
14520
|
+
if (typeof path !== 'string' || path === '') return ''
|
|
14521
|
+
const segments = path.split(/[\\/]/).filter((segment) => segment !== '')
|
|
14522
|
+
return segments.length > 0 ? segments[segments.length - 1] : path
|
|
14523
|
+
}
|
|
14524
|
+
|
|
14525
|
+
/* The pill feeds the SAME keep-warm cache the panel and the prewarmer
|
|
14526
|
+
use (paintRepos / paintPage): a workspace the user has visited — by
|
|
14527
|
+
panel, by pill, or by the session-switch prewarm — re-paints the pill
|
|
14528
|
+
on mount with zero round trips, so switching sessions swaps the text
|
|
14529
|
+
instead of tearing the whole component down and back. The pill only
|
|
14530
|
+
ever MERGES its two fields into an existing page; a panel page with
|
|
14531
|
+
commits must not lose them to a pill poll. */
|
|
14532
|
+
function mergePaintPage(repoRoot, patch) {
|
|
14533
|
+
const entry = readPaintPage(repoRoot)
|
|
14534
|
+
const page = {}
|
|
14535
|
+
if (entry !== undefined) {
|
|
14536
|
+
if (entry.summary !== undefined) page.summary = entry.summary
|
|
14537
|
+
if (entry.branches !== undefined) page.branches = entry.branches
|
|
14538
|
+
if (entry.commits !== undefined) page.commits = entry.commits
|
|
14539
|
+
if (entry.hasMore !== undefined) page.hasMore = entry.hasMore
|
|
14540
|
+
}
|
|
14541
|
+
Object.assign(page, patch)
|
|
14542
|
+
writePaintPage(repoRoot, page)
|
|
14543
|
+
}
|
|
14544
|
+
|
|
14545
|
+
/* The pill's repo pick order (remembered → workspace root when it is a
|
|
14546
|
+
repository → first row), factored out so the cached seed and the fresh
|
|
14547
|
+
discovery apply identically. */
|
|
14548
|
+
function composerPickRepoOf(sessionId, data) {
|
|
14549
|
+
const remembered = readRememberedRepo(sessionId)
|
|
14550
|
+
const paths = data.repos.map((repo) => repo.path)
|
|
14551
|
+
if (remembered !== undefined && paths.indexOf(remembered) >= 0) return remembered
|
|
14552
|
+
if (data.isRepo === true) return data.cwd
|
|
14553
|
+
if (paths.length > 0) return paths[0]
|
|
14554
|
+
return null
|
|
14555
|
+
}
|
|
14556
|
+
|
|
14483
14557
|
function ComposerGitStatus(props) {
|
|
14484
14558
|
const ctx = props.ctx
|
|
14485
14559
|
const t = props.t
|
|
14486
14560
|
const sessionId = typeof props.sessionId === 'string' && props.sessionId !== '' ? props.sessionId : 'default'
|
|
14487
14561
|
const [enabled, setEnabled] = useState(() => composerStatusEnabled())
|
|
14488
14562
|
const [cwd, setCwd] = useState(() => sessionCwdOf(ctx, sessionId))
|
|
14489
|
-
|
|
14490
|
-
|
|
14563
|
+
/* Warm first paint: a workspace the panel, the pill or the prewarmer
|
|
14564
|
+
has already visited starts from the keep-warm cache, so a session
|
|
14565
|
+
switch swaps text instead of flashing through an empty pill. */
|
|
14566
|
+
const [repoState, setRepoState] = useState(() => {
|
|
14567
|
+
const cwd0 = sessionCwdOf(ctx, sessionId)
|
|
14568
|
+
if (cwd0 === undefined) return null
|
|
14569
|
+
const cached = readPaintRepos(cwd0)
|
|
14570
|
+
return cached === undefined ? null : cached
|
|
14571
|
+
})
|
|
14572
|
+
const [repoRoot, setRepoRoot] = useState(() => {
|
|
14573
|
+
const cwd0 = sessionCwdOf(ctx, sessionId)
|
|
14574
|
+
if (cwd0 === undefined) return null
|
|
14575
|
+
const cached = readPaintRepos(cwd0)
|
|
14576
|
+
return cached === undefined ? null : composerPickRepoOf(sessionId, cached)
|
|
14577
|
+
})
|
|
14491
14578
|
const [summary, setSummary] = useState(null)
|
|
14492
14579
|
const [menu, setMenu] = useState(null)
|
|
14493
14580
|
const [branchList, setBranchList] = useState(null)
|
|
@@ -14527,19 +14614,24 @@ window.__ModuleLoader__.load({
|
|
|
14527
14614
|
useEffect(() => {
|
|
14528
14615
|
if (cwd === undefined || enabled !== true) { setRepoState(null); setRepoRoot(null); setSummary(null); return undefined }
|
|
14529
14616
|
let cancelled = false
|
|
14617
|
+
const applyRepos = (data) => {
|
|
14618
|
+
setRepoState(data)
|
|
14619
|
+
setRepoRoot(composerPickRepoOf(sessionId, data))
|
|
14620
|
+
}
|
|
14621
|
+
/* Warm seed first: a workspace the panel, the pill or the prewarmer
|
|
14622
|
+
already visited paints its labels this tick; the request below
|
|
14623
|
+
silently re-proves them. A failed refresh must not evict a good
|
|
14624
|
+
seed — only fall back to the empty state when nothing is on
|
|
14625
|
+
screen. */
|
|
14626
|
+
const seeded = readPaintRepos(cwd)
|
|
14627
|
+
if (seeded !== undefined) applyRepos(seeded)
|
|
14530
14628
|
void (async () => {
|
|
14531
14629
|
try {
|
|
14532
14630
|
const data = await request('repos', { cwd: cwd })
|
|
14533
14631
|
if (cancelled === true) return
|
|
14534
|
-
|
|
14535
|
-
|
|
14536
|
-
|
|
14537
|
-
let pick = null
|
|
14538
|
-
if (remembered !== undefined && paths.indexOf(remembered) >= 0) pick = remembered
|
|
14539
|
-
else if (data.isRepo === true) pick = data.cwd
|
|
14540
|
-
else if (paths.length > 0) pick = paths[0]
|
|
14541
|
-
setRepoRoot(pick)
|
|
14542
|
-
} catch (error) { void error; if (cancelled !== true) setRepoState({ repos: [], isRepo: false }) }
|
|
14632
|
+
writePaintRepos(cwd, data)
|
|
14633
|
+
applyRepos(data)
|
|
14634
|
+
} catch (error) { void error; if (cancelled !== true && seeded === undefined) setRepoState({ repos: [], isRepo: false }) }
|
|
14543
14635
|
})()
|
|
14544
14636
|
return () => { cancelled = true }
|
|
14545
14637
|
}, [cwd, sessionId, enabled])
|
|
@@ -14555,8 +14647,15 @@ window.__ModuleLoader__.load({
|
|
|
14555
14647
|
const data = await request('summary', { cwd: cwd, repoRoot: repoRoot, stats: false })
|
|
14556
14648
|
if (cancelled === true) return
|
|
14557
14649
|
setSummary(data)
|
|
14650
|
+
/* Stale-while-revalidate both ways: our poll keeps the warm page
|
|
14651
|
+
(panel / prewarm / seed) fresh, and a warm page seeds the NEXT
|
|
14652
|
+
mount with zero round trips. Merge only our field — a panel
|
|
14653
|
+
page keeps its commits. */
|
|
14654
|
+
mergePaintPage(repoRoot, { summary: data })
|
|
14558
14655
|
} catch (error) { void error }
|
|
14559
14656
|
}
|
|
14657
|
+
const seed = readPaintPage(repoRoot)
|
|
14658
|
+
if (seed !== undefined && seed.summary !== undefined && seed.summary !== null) setSummary(seed.summary)
|
|
14560
14659
|
void load()
|
|
14561
14660
|
const timer = setInterval(() => { void load() }, COMPOSER_POLL_MS)
|
|
14562
14661
|
return () => { cancelled = true; clearInterval(timer) }
|
|
@@ -14568,7 +14667,7 @@ window.__ModuleLoader__.load({
|
|
|
14568
14667
|
const watcher = (sid, root) => {
|
|
14569
14668
|
if (sid !== sessionId || typeof root !== 'string' || root === '') return
|
|
14570
14669
|
setRepoRoot((current) => (current === root ? current : root))
|
|
14571
|
-
setSummary(
|
|
14670
|
+
setSummary(seedSummaryOf(root))
|
|
14572
14671
|
setBranchList(null)
|
|
14573
14672
|
setTick((value) => value + 1)
|
|
14574
14673
|
}
|
|
@@ -14610,7 +14709,12 @@ window.__ModuleLoader__.load({
|
|
|
14610
14709
|
if (data === null || (data.isRepo !== true && (data.repos === undefined || data.repos.length === 0))) return null
|
|
14611
14710
|
const repoPaths = data.repos.map((repo) => repo.path)
|
|
14612
14711
|
const singleRepo = repoPaths.length <= 1
|
|
14613
|
-
|
|
14712
|
+
/* The pill shows the repository's OWN name — the origin URL's leaf
|
|
14713
|
+
(summary.origin) — so a clone renamed on disk still reads as the
|
|
14714
|
+
repo it came from; the folder basename is only the fallback for
|
|
14715
|
+
repos without any remote. */
|
|
14716
|
+
const originName = summary !== null && typeof summary.origin === 'string' && summary.origin !== '' ? summary.origin : ''
|
|
14717
|
+
const repoName = originName !== '' ? originName : baseNameOf(repoRoot)
|
|
14614
14718
|
const branchName = summary !== null && typeof summary.branch === 'string' && summary.branch !== '' ? summary.branch : null
|
|
14615
14719
|
const detached = summary !== null && summary.detached === true
|
|
14616
14720
|
const ahead = summary !== null && typeof summary.ahead === 'number' ? summary.ahead : 0
|
|
@@ -14621,12 +14725,17 @@ window.__ModuleLoader__.load({
|
|
|
14621
14725
|
|
|
14622
14726
|
const openBranchMenu = async () => {
|
|
14623
14727
|
setMenu('branch')
|
|
14624
|
-
|
|
14625
|
-
|
|
14728
|
+
/* A warm page (panel or prewarm) fills the list this tick; the fetch
|
|
14729
|
+
below re-proves it. On failure keep whatever is showing. */
|
|
14730
|
+
const seed = readPaintPage(repoRoot)
|
|
14731
|
+
const seeded = seed !== undefined && seed.branches !== undefined && seed.branches !== null ? seed.branches : null
|
|
14732
|
+
setBranchList(seeded)
|
|
14733
|
+
setBranchLoading(seeded === null)
|
|
14626
14734
|
try {
|
|
14627
14735
|
const branchesData = await request('branches', { cwd: cwd, repoRoot: repoRoot })
|
|
14628
14736
|
setBranchList(branchesData)
|
|
14629
|
-
|
|
14737
|
+
mergePaintPage(repoRoot, { branches: branchesData })
|
|
14738
|
+
} catch (caught) { void caught } finally { setBranchLoading(false) }
|
|
14630
14739
|
}
|
|
14631
14740
|
const checkoutBranch = async (name) => {
|
|
14632
14741
|
if (repoRoot === null || checkingOut !== '') return
|
|
@@ -14641,10 +14750,18 @@ window.__ModuleLoader__.load({
|
|
|
14641
14750
|
setError(caught instanceof Error ? caught.message : String(caught))
|
|
14642
14751
|
} finally { setCheckingOut('') }
|
|
14643
14752
|
}
|
|
14753
|
+
/* Swap to a warm summary synchronously when one exists — the null gap
|
|
14754
|
+
between pick and effect would paint one empty frame. */
|
|
14755
|
+
const seedSummaryOf = (root) => {
|
|
14756
|
+
if (typeof root !== 'string' || root === '') return null
|
|
14757
|
+
const seed = readPaintPage(root)
|
|
14758
|
+
return seed !== undefined && seed.summary !== undefined && seed.summary !== null ? seed.summary : null
|
|
14759
|
+
}
|
|
14760
|
+
|
|
14644
14761
|
const pickRepoRoot = (path) => {
|
|
14645
14762
|
setMenu(null)
|
|
14646
14763
|
setRepoRoot(path)
|
|
14647
|
-
setSummary(
|
|
14764
|
+
setSummary(seedSummaryOf(path))
|
|
14648
14765
|
setBranchList(null)
|
|
14649
14766
|
setTick((value) => value + 1)
|
|
14650
14767
|
rememberRepoShared(sessionId, path)
|
|
@@ -14676,22 +14793,32 @@ window.__ModuleLoader__.load({
|
|
|
14676
14793
|
behind > 0 ? E('span', { className: 'dig-cs-badge dig-cs-behind', title: t('composer.behindHint') }, '↓' + String(behind)) : null,
|
|
14677
14794
|
ahead > 0 ? E('span', { className: 'dig-cs-badge dig-cs-ahead', title: t('composer.aheadHint') }, '↑' + String(ahead)) : null,
|
|
14678
14795
|
error !== '' ? E('span', { className: 'dig-cs-error', title: error }, '×') : null,
|
|
14679
|
-
menu === 'repo' ? E('div', { className: 'dig-cs-menu' },
|
|
14680
|
-
|
|
14681
|
-
|
|
14682
|
-
|
|
14683
|
-
|
|
14684
|
-
|
|
14685
|
-
|
|
14686
|
-
|
|
14687
|
-
|
|
14688
|
-
E(
|
|
14689
|
-
|
|
14690
|
-
|
|
14796
|
+
menu === 'repo' ? E('div', { className: 'dig-cs-menu', 'data-cordis-panel': '' },
|
|
14797
|
+
/* v0.19.2: one row per checkout — the repository's own name first
|
|
14798
|
+
(origin URL basename, falling back to the directory name), and the
|
|
14799
|
+
single-level directory name as a small sub line only when the two
|
|
14800
|
+
differ (two clones of one repository stay tellable apart). The
|
|
14801
|
+
full path lives in the tooltip, not in the row. */
|
|
14802
|
+
data.repos.map((row) => {
|
|
14803
|
+
const dirName = baseNameOf(row.path)
|
|
14804
|
+
const displayName = row.origin !== undefined && row.origin !== '' ? row.origin : dirName
|
|
14805
|
+
return E('button', {
|
|
14806
|
+
key: row.path,
|
|
14807
|
+
type: 'button',
|
|
14808
|
+
className: 'dig-cs-item',
|
|
14809
|
+
'data-current': row.path === repoRoot ? 'true' : 'false',
|
|
14810
|
+
title: row.path,
|
|
14811
|
+
onClick: () => pickRepoRoot(row.path),
|
|
14812
|
+
},
|
|
14813
|
+
E(Icon, { name: 'commit', size: 11 }),
|
|
14814
|
+
E('span', { className: 'dig-cs-item-main' },
|
|
14815
|
+
E('span', { className: 'dig-cs-item-name' }, displayName),
|
|
14816
|
+
dirName !== displayName ? E('span', { className: 'dig-cs-item-sub' }, dirName) : null))
|
|
14817
|
+
}))
|
|
14691
14818
|
: null,
|
|
14692
14819
|
menu === 'branch' ? (branchLoading === true || branchList === null
|
|
14693
|
-
? E('div', { className: 'dig-cs-menu' }, E('div', { className: 'dig-cs-item dig-cs-static' }, '…'))
|
|
14694
|
-
: E('div', { className: 'dig-cs-menu' },
|
|
14820
|
+
? E('div', { className: 'dig-cs-menu', 'data-cordis-panel': '' }, E('div', { className: 'dig-cs-item dig-cs-static' }, '…'))
|
|
14821
|
+
: E('div', { className: 'dig-cs-menu', 'data-cordis-panel': '' },
|
|
14695
14822
|
branchList.local.map((entry) => E('button', {
|
|
14696
14823
|
key: entry.name,
|
|
14697
14824
|
type: 'button',
|
package/src/index.js
CHANGED
|
@@ -600,6 +600,35 @@ async function operationOf(root) {
|
|
|
600
600
|
|
|
601
601
|
/* ============================== methods ============================== */
|
|
602
602
|
|
|
603
|
+
/* The repository's own name, cut from its origin URL — a cloned folder that
|
|
604
|
+
was renamed on disk still reports the name the remote gave it (the leaf of
|
|
605
|
+
the URL, minus a trailing .git). scp-style (user@host:path), URL-style and
|
|
606
|
+
plain local paths all cut on the last slash or colon. */
|
|
607
|
+
function repoNameFromUrl(url) {
|
|
608
|
+
if (typeof url !== 'string' || url.trim() === '') return ''
|
|
609
|
+
const tail = url.trim().replace(/\/+$/, '').replace(/\.git$/i, '')
|
|
610
|
+
const segments = tail.split(/[\\/:]/).filter((segment) => segment !== '')
|
|
611
|
+
return segments.length > 0 ? segments[segments.length - 1] : ''
|
|
612
|
+
}
|
|
613
|
+
/* origin first, then the first configured remote, then '' — the client
|
|
614
|
+
falls back to the folder basename when this is empty. Both reads are
|
|
615
|
+
local config reads, never network. */
|
|
616
|
+
async function originNameOf(root) {
|
|
617
|
+
try {
|
|
618
|
+
const origin = await git(root, ['remote', 'get-url', 'origin'])
|
|
619
|
+
const name = repoNameFromUrl(origin)
|
|
620
|
+
if (name !== '') return name
|
|
621
|
+
} catch (error) { void error }
|
|
622
|
+
try {
|
|
623
|
+
const remotesOut = await runGit(root, ['remote'])
|
|
624
|
+
if (remotesOut.code !== 0) return ''
|
|
625
|
+
const first = remotesOut.stdout.split('\n').map((line) => line.trim()).filter((line) => line !== '')[0]
|
|
626
|
+
if (first === undefined) return ''
|
|
627
|
+
const urlOut = await runGit(root, ['remote', 'get-url', first])
|
|
628
|
+
return urlOut.code === 0 ? repoNameFromUrl(urlOut.stdout.trim()) : ''
|
|
629
|
+
} catch (error) { void error; return '' }
|
|
630
|
+
}
|
|
631
|
+
|
|
603
632
|
async function summary(payload) {
|
|
604
633
|
const cwd = cwdOf(payload)
|
|
605
634
|
const root = await repoRootOf(cwd, payload)
|
|
@@ -614,7 +643,7 @@ async function summary(payload) {
|
|
|
614
643
|
large `status` scan no longer serialises behind five cheap calls
|
|
615
644
|
(issue #11: the sum of ~ten sequential commands became the max of two
|
|
616
645
|
chains). */
|
|
617
|
-
const [branchInfo, statusOut, unstagedStat, stagedStat, ignored, stashCount, worktrees, operation, head] = await Promise.all([
|
|
646
|
+
const [branchInfo, statusOut, unstagedStat, stagedStat, ignored, stashCount, worktrees, operation, head, originName] = await Promise.all([
|
|
618
647
|
(async () => {
|
|
619
648
|
const branch = await currentBranchOf(root)
|
|
620
649
|
const detached = branch === 'HEAD'
|
|
@@ -633,6 +662,7 @@ async function summary(payload) {
|
|
|
633
662
|
checkouts — the panel's own actions already refresh everything, but
|
|
634
663
|
nothing else watches the repo. Empty string on an unborn branch. */
|
|
635
664
|
git(root, ['rev-parse', 'HEAD']).then((out) => out.trim()).catch(() => ''),
|
|
665
|
+
originNameOf(root),
|
|
636
666
|
])
|
|
637
667
|
const groups = classify(parsePorcelain(statusOut))
|
|
638
668
|
return {
|
|
@@ -644,6 +674,9 @@ async function summary(payload) {
|
|
|
644
674
|
ahead: branchInfo.tracking.ahead,
|
|
645
675
|
behind: branchInfo.tracking.behind,
|
|
646
676
|
head: head,
|
|
677
|
+
/* The repository's own name (v0.19.1): the origin URL's leaf, so a
|
|
678
|
+
renamed clone still shows the real repo name, not the folder's. */
|
|
679
|
+
origin: originName,
|
|
647
680
|
changes: {
|
|
648
681
|
staged: decorate(groups.staged, stagedStat),
|
|
649
682
|
unstaged: decorate(groups.unstaged, unstagedStat),
|
|
@@ -1401,6 +1434,12 @@ function repoRow(root, submodulePath, kind) {
|
|
|
1401
1434
|
name: submodulePath === undefined ? path.basename(root) : submodulePath,
|
|
1402
1435
|
label: submodulePath === undefined ? path.basename(root) : submodulePath.split('/').pop(),
|
|
1403
1436
|
branch: null,
|
|
1437
|
+
/** The checkout's own repository name, cut from its origin URL (v0.19.2):
|
|
1438
|
+
* the picker shows this first and falls back to the directory name, with
|
|
1439
|
+
* the directory name as the small sub line whenever the two differ — so
|
|
1440
|
+
* two clones of one repository stay tellable apart. Filled in by the
|
|
1441
|
+
* scan below (an origin-less checkout keeps the empty string). */
|
|
1442
|
+
origin: '',
|
|
1404
1443
|
kind,
|
|
1405
1444
|
...(submodulePath === undefined ? {} : { submodulePath }),
|
|
1406
1445
|
}
|
|
@@ -1507,6 +1546,11 @@ async function scanRepos(cwd) {
|
|
|
1507
1546
|
const capped = rows.slice(0, REPO_LIST_LIMIT).map((row) => ({ ...row }))
|
|
1508
1547
|
const branches = await Promise.all(capped.map((row) => branchOrNull(row.path)))
|
|
1509
1548
|
for (let index = 0; index < capped.length; index += 1) capped[index].branch = branches[index]
|
|
1549
|
+
/* Origin names ride the same one-call-per-row pass as the branches: a local
|
|
1550
|
+
`git remote get-url` read, cheap, and the 15s cache means repeated picker
|
|
1551
|
+
opens pay for it once. */
|
|
1552
|
+
const origins = await Promise.all(capped.map((row) => originNameOf(row.path)))
|
|
1553
|
+
for (let index = 0; index < capped.length; index += 1) capped[index].origin = origins[index]
|
|
1510
1554
|
repoScanCache.set(cwd, { at: Date.now(), rows: capped })
|
|
1511
1555
|
return { cached: false, rows: capped.map((row) => ({ ...row })) }
|
|
1512
1556
|
}
|