dsh-remote-vs 0.0.0-stage → 0.8.3
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/LICENSE +21 -0
- package/README.en.md +385 -0
- package/README.md +347 -2
- package/cordis.patch.yml +259 -0
- package/icon.svg +7 -0
- package/lib/client.js +62602 -0
- package/lib/index.js +4287 -0
- package/lib/p2-bridge-refs.js +10 -0
- package/lib/p2-bridge.js +16 -0
- package/lib/p2-router-fs.js +252 -0
- package/lib/p2-router-refs.js +466 -0
- package/lib/p2-router-spill.js +62 -0
- package/lib/p2-router-subprocess.js +64 -0
- package/lib/p2-shell-bash.js +24 -0
- package/lib/ssh-proc-arI5lm4W.js +632 -0
- package/lib/types/changes/recorder.d.ts +240 -0
- package/lib/types/changes/status.d.ts +201 -0
- package/lib/types/changes/text-diff.d.ts +65 -0
- package/lib/types/client/CodeEditor.d.ts +125 -0
- package/lib/types/client/Dialog.d.ts +14 -0
- package/lib/types/client/DirectoryFlow.d.ts +30 -0
- package/lib/types/client/EditorPane.d.ts +30 -0
- package/lib/types/client/HostsView.d.ts +10 -0
- package/lib/types/client/NativeExplorer.d.ts +39 -0
- package/lib/types/client/RemoteDirectoryPicker.d.ts +16 -0
- package/lib/types/client/RemoteEditorView.d.ts +20 -0
- package/lib/types/client/RemoteExplorer.d.ts +18 -0
- package/lib/types/client/RemoteRoot.d.ts +16 -0
- package/lib/types/client/SearchView.d.ts +14 -0
- package/lib/types/client/SettingsHosts.d.ts +15 -0
- package/lib/types/client/TerminalView.d.ts +16 -0
- package/lib/types/client/WorkspaceWizard.d.ts +16 -0
- package/lib/types/client/WorkspacesView.d.ts +14 -0
- package/lib/types/client/XTerm.d.ts +15 -0
- package/lib/types/client/api.d.ts +162 -0
- package/lib/types/client/changes/ChangesDock.d.ts +35 -0
- package/lib/types/client/changes/model.d.ts +36 -0
- package/lib/types/client/changes/undo-affordance.d.ts +48 -0
- package/lib/types/client/changes/undo-context.d.ts +25 -0
- package/lib/types/client/composer.d.ts +165 -0
- package/lib/types/client/i18n.d.ts +17 -0
- package/lib/types/client/index.d.ts +14 -0
- package/lib/types/client/remote-file-open-target.d.ts +247 -0
- package/lib/types/client/remote-file-open.d.ts +35 -0
- package/lib/types/client/reveal.d.ts +15 -0
- package/lib/types/client/scm/DiffView.d.ts +24 -0
- package/lib/types/client/scm/HistoryPanel.d.ts +12 -0
- package/lib/types/client/scm/RepoPicker.d.ts +24 -0
- package/lib/types/client/scm/ScmPanel.d.ts +15 -0
- package/lib/types/client/scm/actions.d.ts +170 -0
- package/lib/types/client/scm/diff-model.d.ts +29 -0
- package/lib/types/client/scm/stage-queue.d.ts +99 -0
- package/lib/types/client/scm/status-map.d.ts +157 -0
- package/lib/types/client/scm/untracked-dirs.d.ts +72 -0
- package/lib/types/client/selection-popup.d.ts +61 -0
- package/lib/types/client/store.d.ts +437 -0
- package/lib/types/client/theme.d.ts +15 -0
- package/lib/types/client/ui.d.ts +46 -0
- package/lib/types/client/upload-plan.d.ts +78 -0
- package/lib/types/client/upload-source.d.ts +46 -0
- package/lib/types/client/use-remote-chat-target.d.ts +17 -0
- package/lib/types/client/vsc/FindWidget.d.ts +81 -0
- package/lib/types/client/vsc/change-marks.d.ts +150 -0
- package/lib/types/client/vsc/editor-langs.d.ts +5 -0
- package/lib/types/client/vsc/editor-palette.d.ts +37 -0
- package/lib/types/client/vsc/editor-theme.d.ts +32 -0
- package/lib/types/client/vsc/fallback-highlight.d.ts +26 -0
- package/lib/types/client/vsc/kit.d.ts +195 -0
- package/lib/types/client/vsc/markdown.d.ts +11 -0
- package/lib/types/client/vsc/reveal-line.d.ts +38 -0
- package/lib/types/client/vsc/tokenize.d.ts +104 -0
- package/lib/types/deploy.d.ts +94 -0
- package/lib/types/fs-ops.d.ts +8 -0
- package/lib/types/git-format.d.ts +34 -0
- package/lib/types/git-ops.d.ts +183 -0
- package/lib/types/http.d.ts +14 -0
- package/lib/types/importers.d.ts +20 -0
- package/lib/types/index.d.ts +87 -0
- package/lib/types/keygen.d.ts +19 -0
- package/lib/types/loopback.d.ts +7 -0
- package/lib/types/p2/bridge-refs.d.ts +21 -0
- package/lib/types/p2/bridge.d.ts +24 -0
- package/lib/types/p2/file-refs.d.ts +73 -0
- package/lib/types/p2/helper-rpc.d.ts +63 -0
- package/lib/types/p2/in-flight-gate.d.ts +25 -0
- package/lib/types/p2/router-fs.d.ts +92 -0
- package/lib/types/p2/router-refs.d.ts +45 -0
- package/lib/types/p2/router-spill.d.ts +11 -0
- package/lib/types/p2/router-subprocess.d.ts +23 -0
- package/lib/types/p2/shell-bash.d.ts +16 -0
- package/lib/types/p2/ssh-proc.d.ts +22 -0
- package/lib/types/p2/worlds.d.ts +179 -0
- package/lib/types/paths.d.ts +12 -0
- package/lib/types/pool.d.ts +105 -0
- package/lib/types/protocol.d.ts +452 -0
- package/lib/types/routes.d.ts +27 -0
- package/lib/types/search-ops.d.ts +68 -0
- package/lib/types/sftp.d.ts +90 -0
- package/lib/types/store.d.ts +66 -0
- package/lib/types/terminal.d.ts +51 -0
- package/lib/types/timeouts.d.ts +83 -0
- package/lib/types/tools.d.ts +41 -0
- package/lib/worlds-PUumRIh7.js +18765 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +189 -4
- package/src/changes/recorder.ts +902 -0
- package/src/changes/status.ts +448 -0
- package/src/changes/text-diff.ts +377 -0
- package/src/client/CodeEditor.tsx +923 -0
- package/src/client/Dialog.tsx +76 -0
- package/src/client/DirectoryFlow.tsx +431 -0
- package/src/client/EditorPane.tsx +512 -0
- package/src/client/HostsView.tsx +312 -0
- package/src/client/NativeExplorer.tsx +121 -0
- package/src/client/RemoteDirectoryPicker.tsx +157 -0
- package/src/client/RemoteEditorView.tsx +54 -0
- package/src/client/RemoteExplorer.tsx +1398 -0
- package/src/client/RemoteRoot.tsx +389 -0
- package/src/client/SearchView.tsx +407 -0
- package/src/client/SettingsHosts.tsx +136 -0
- package/src/client/TerminalView.tsx +308 -0
- package/src/client/WorkspaceWizard.tsx +199 -0
- package/src/client/WorkspacesView.tsx +93 -0
- package/src/client/XTerm.tsx +308 -0
- package/src/client/api.ts +331 -0
- package/src/client/changes/ChangesDock.tsx +348 -0
- package/src/client/changes/changes.css +230 -0
- package/src/client/changes/model.ts +347 -0
- package/src/client/changes/undo-affordance.ts +389 -0
- package/src/client/changes/undo-context.ts +34 -0
- package/src/client/composer.ts +436 -0
- package/src/client/css.d.ts +7 -0
- package/src/client/i18n.ts +702 -0
- package/src/client/index.tsx +286 -0
- package/src/client/remote-file-open-target.ts +553 -0
- package/src/client/remote-file-open.ts +261 -0
- package/src/client/reveal.ts +33 -0
- package/src/client/scm/DiffView.tsx +570 -0
- package/src/client/scm/HistoryPanel.tsx +353 -0
- package/src/client/scm/RepoPicker.tsx +238 -0
- package/src/client/scm/ScmPanel.tsx +857 -0
- package/src/client/scm/actions.ts +460 -0
- package/src/client/scm/diff-model.ts +206 -0
- package/src/client/scm/stage-queue.ts +264 -0
- package/src/client/scm/status-map.ts +381 -0
- package/src/client/scm/untracked-dirs.ts +139 -0
- package/src/client/selection-popup.ts +157 -0
- package/src/client/store.tsx +1670 -0
- package/src/client/theme.ts +54 -0
- package/src/client/ui.tsx +136 -0
- package/src/client/upload-plan.ts +153 -0
- package/src/client/upload-source.ts +145 -0
- package/src/client/use-remote-chat-target.ts +153 -0
- package/src/client/vsc/FindWidget.tsx +446 -0
- package/src/client/vsc/change-marks.ts +586 -0
- package/src/client/vsc/editor-langs.ts +137 -0
- package/src/client/vsc/editor-palette.ts +107 -0
- package/src/client/vsc/editor-theme.ts +93 -0
- package/src/client/vsc/fallback-highlight.ts +145 -0
- package/src/client/vsc/kit.tsx +777 -0
- package/src/client/vsc/markdown.tsx +172 -0
- package/src/client/vsc/reveal-line.ts +142 -0
- package/src/client/vsc/tokenize.ts +569 -0
- package/src/client/vsc/vsc.css +2669 -0
- package/src/deploy.ts +481 -0
- package/src/fs-ops.ts +57 -0
- package/src/git-format.ts +205 -0
- package/src/git-ops.ts +693 -0
- package/src/http.ts +55 -0
- package/src/importers.ts +141 -0
- package/src/index.ts +442 -0
- package/src/keygen.ts +77 -0
- package/src/loopback.ts +47 -0
- package/src/p2/bridge-refs.ts +27 -0
- package/src/p2/bridge.ts +32 -0
- package/src/p2/file-refs.ts +392 -0
- package/src/p2/helper-rpc.ts +394 -0
- package/src/p2/in-flight-gate.ts +63 -0
- package/src/p2/router-fs.ts +327 -0
- package/src/p2/router-refs.ts +166 -0
- package/src/p2/router-spill.ts +75 -0
- package/src/p2/router-subprocess.ts +92 -0
- package/src/p2/shell-bash.ts +30 -0
- package/src/p2/ssh-proc.ts +802 -0
- package/src/p2/worlds.ts +735 -0
- package/src/paths.ts +50 -0
- package/src/pool.ts +811 -0
- package/src/protocol.ts +460 -0
- package/src/routes.ts +802 -0
- package/src/search-ops.ts +220 -0
- package/src/sftp.ts +677 -0
- package/src/store.ts +364 -0
- package/src/terminal.ts +291 -0
- package/src/timeouts.ts +122 -0
- package/src/tools.ts +320 -0
- package/vendor/dsh-remote-helpers/dsh-ssh-helper-0.2.1-alpha.2-linux-x64.helper.json +16 -0
- package/vendor/dsh-remote-helpers/dsh-ssh-helper-0.2.1-alpha.2-linux-x64.tar.gz +4 -0
- package/vendor/dsh-remote-helpers/dsh-ssh-helper-0.2.1-alpha.2-linux-x64.tar.gz.sha256 +1 -0
- package/vendor/dsh-remote-helpers/glibc-ceiling.patch +31 -0
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stage / unstage coordinator: instant local feedback, ONE cheap verification.
|
|
3
|
+
*
|
|
4
|
+
* The reported problem: pressing `+` on a single file in a large repository
|
|
5
|
+
* (AOSP monorepo) fired FOUR repository-wide git commands — the `git add`
|
|
6
|
+
* itself, a full `git status` (through the panel's `refreshAll` →
|
|
7
|
+
* `store.afterWrite()`), and the three extras loaders (stash list, branches,
|
|
8
|
+
* remotes) — so one click cost minutes and locked the panel behind `gitBusy`.
|
|
9
|
+
* Nothing about staging one file needs any of them.
|
|
10
|
+
*
|
|
11
|
+
* What happens instead, per click:
|
|
12
|
+
* 1. the row moves between "changes" and "staged changes" IMMEDIATELY, in the
|
|
13
|
+
* local status model (`sink.apply`), before anything leaves the browser;
|
|
14
|
+
* 2. the real `git add` / `git restore --staged` goes out, serialized per
|
|
15
|
+
* repository so two clicks can never reorder each other;
|
|
16
|
+
* 3. clicks landing inside one window (250 ms) coalesce into ONE path-limited
|
|
17
|
+
* `git status -- …` probe, whose cost tracks the touched paths rather than
|
|
18
|
+
* the repository;
|
|
19
|
+
* 4. a path the probe contradicts is put back to what the repository actually
|
|
20
|
+
* says — and the user is told. An optimistic update may be fast; it may
|
|
21
|
+
* never quietly lie.
|
|
22
|
+
*
|
|
23
|
+
* No React, no fetch: the git verbs and the two local effects are injected, so
|
|
24
|
+
* the whole policy is drivable by `scripts/check-scm-stage.mjs` with a stubbed
|
|
25
|
+
* port and a fake clock.
|
|
26
|
+
*/
|
|
27
|
+
import { parseStatusDetail } from '../../git-ops.ts'
|
|
28
|
+
import { stagedStateOf } from './status-map.ts'
|
|
29
|
+
|
|
30
|
+
/** The repository a stage/unstage acts on. */
|
|
31
|
+
export interface ScmTarget {
|
|
32
|
+
/** Host alias. */
|
|
33
|
+
alias: string
|
|
34
|
+
/** Absolute remote repository root (git runs here). */
|
|
35
|
+
root: string
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** The git verbs the coordinator drives. */
|
|
39
|
+
export interface StagePort {
|
|
40
|
+
/** `git add -A -- <paths>`. */
|
|
41
|
+
stage: (target: ScmTarget, paths: readonly string[]) => Promise<void>
|
|
42
|
+
/** `git restore --staged -- <paths>`. */
|
|
43
|
+
unstage: (target: ScmTarget, paths: readonly string[]) => Promise<void>
|
|
44
|
+
/** Raw `-z` stdout of the path-limited status (`probeStatusPaths`). */
|
|
45
|
+
probe: (target: ScmTarget, paths: readonly string[]) => Promise<string>
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** A verification outcome the user has to know about. */
|
|
49
|
+
export interface StageNotice {
|
|
50
|
+
/**
|
|
51
|
+
* `mismatch`: the repository disagreed with the local move, which was then
|
|
52
|
+
* corrected. `unverified`: the check itself could not run, so the local state
|
|
53
|
+
* stands unconfirmed.
|
|
54
|
+
*/
|
|
55
|
+
kind: 'mismatch' | 'unverified'
|
|
56
|
+
/** Repo-relative paths the notice is about, in click order. */
|
|
57
|
+
paths: readonly string[]
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** The two local effects: the status model, and the user. */
|
|
61
|
+
export interface StageSink {
|
|
62
|
+
/** Move paths between the index and worktree halves of the local model. */
|
|
63
|
+
apply: (paths: readonly string[], staged: boolean) => void
|
|
64
|
+
/**
|
|
65
|
+
* Report one verification outcome.
|
|
66
|
+
*
|
|
67
|
+
* A structured notice rather than a sentence: the wording belongs to the
|
|
68
|
+
* caller (its own i18n keys, which have to be literal `t()` calls in the
|
|
69
|
+
* source for the i18n check to see them), while this module stays free of
|
|
70
|
+
* React, fetch and copy.
|
|
71
|
+
*/
|
|
72
|
+
report: (notice: StageNotice) => void
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Timer injection, so a check can drive the debounce deterministically. */
|
|
76
|
+
export interface StageClock {
|
|
77
|
+
set: (fn: () => void, ms: number) => unknown
|
|
78
|
+
clear: (handle: unknown) => void
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** The debounce window: how long clicks keep merging into one probe. */
|
|
82
|
+
export const DEFAULT_STAGE_WINDOW_MS = 250
|
|
83
|
+
|
|
84
|
+
/** One pending click. */
|
|
85
|
+
interface Job {
|
|
86
|
+
target: ScmTarget
|
|
87
|
+
paths: readonly string[]
|
|
88
|
+
staged: boolean
|
|
89
|
+
/** The real verb, already in flight (ordered per repository). */
|
|
90
|
+
write: Promise<void>
|
|
91
|
+
resolve: () => void
|
|
92
|
+
reject: (error: unknown) => void
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Repository identity for grouping (alias + root, one line each). */
|
|
96
|
+
function targetKey(target: ScmTarget): string {
|
|
97
|
+
return `${target.alias}\n${target.root}`
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Coordinates optimistic stage/unstage with a debounced, path-limited check.
|
|
102
|
+
*
|
|
103
|
+
* One instance per client: the panel's clicks, the store's status model and the
|
|
104
|
+
* user notifications all meet here.
|
|
105
|
+
*/
|
|
106
|
+
export class StageCoordinator {
|
|
107
|
+
private readonly port: StagePort
|
|
108
|
+
private readonly sink: StageSink
|
|
109
|
+
private readonly clock: StageClock
|
|
110
|
+
private readonly windowMs: number
|
|
111
|
+
/** Clicks waiting for the current window to close. */
|
|
112
|
+
private pending: Job[] = []
|
|
113
|
+
private timer: unknown
|
|
114
|
+
/** Per-repository write chain: the second click waits for the first. */
|
|
115
|
+
private readonly chains = new Map<string, Promise<void>>()
|
|
116
|
+
|
|
117
|
+
constructor(
|
|
118
|
+
port: StagePort,
|
|
119
|
+
sink: StageSink,
|
|
120
|
+
options: { clock?: StageClock; windowMs?: number } = {},
|
|
121
|
+
) {
|
|
122
|
+
this.port = port
|
|
123
|
+
this.sink = sink
|
|
124
|
+
this.clock = options.clock ?? {
|
|
125
|
+
set: (fn, ms) => setTimeout(fn, ms),
|
|
126
|
+
clear: handle => { clearTimeout(handle as ReturnType<typeof setTimeout>) },
|
|
127
|
+
}
|
|
128
|
+
this.windowMs = options.windowMs ?? DEFAULT_STAGE_WINDOW_MS
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Stage or unstage paths: local first, real verb in flight, verification once
|
|
133
|
+
* the window closes.
|
|
134
|
+
*
|
|
135
|
+
* @param target - repository the paths belong to.
|
|
136
|
+
* @param paths - repo-relative paths (a directory row may keep its `/`).
|
|
137
|
+
* @param staged - true to stage, false to unstage.
|
|
138
|
+
* @returns a promise that settles when this click's verification is in:
|
|
139
|
+
* rejected when the git verb itself failed (the caller reports it), and
|
|
140
|
+
* resolved when the verb landed — whether or not the probe agreed (a
|
|
141
|
+
* disagreement is corrected and notified here, not thrown).
|
|
142
|
+
*/
|
|
143
|
+
submit(target: ScmTarget, paths: readonly string[], staged: boolean): Promise<void> {
|
|
144
|
+
if (paths.length === 0) return Promise.resolve()
|
|
145
|
+
const list = [...paths]
|
|
146
|
+
// The write is queued BEFORE the local patch so the two orders cannot
|
|
147
|
+
// invert: the chain's first link runs on a microtask, after this frame.
|
|
148
|
+
const write = this.enqueue(target, () => (staged ? this.port.stage : this.port.unstage)(target, list))
|
|
149
|
+
let resolve: () => void = () => {}
|
|
150
|
+
let reject: (error: unknown) => void = () => {}
|
|
151
|
+
const settled = new Promise<void>((done, fail) => {
|
|
152
|
+
resolve = done
|
|
153
|
+
reject = fail
|
|
154
|
+
})
|
|
155
|
+
this.sink.apply(list, staged)
|
|
156
|
+
this.pending.push({ target, paths: list, staged, write, resolve, reject })
|
|
157
|
+
this.arm()
|
|
158
|
+
return settled
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Verify everything submitted so far with ONE path-limited probe per
|
|
163
|
+
* repository. Called by the debounce timer; exposed so a caller (or a check)
|
|
164
|
+
* can force the window closed.
|
|
165
|
+
*/
|
|
166
|
+
async flush(): Promise<void> {
|
|
167
|
+
if (this.timer !== undefined) {
|
|
168
|
+
this.clock.clear(this.timer)
|
|
169
|
+
this.timer = undefined
|
|
170
|
+
}
|
|
171
|
+
const batch = this.pending
|
|
172
|
+
this.pending = []
|
|
173
|
+
if (batch.length === 0) return
|
|
174
|
+
// The probe must never run before the writes it verifies have landed: on a
|
|
175
|
+
// slow repository a `git add` can outlast the debounce window by seconds.
|
|
176
|
+
const outcomes = await Promise.all(batch.map(job => job.write.then(() => undefined, (error: unknown) => error)))
|
|
177
|
+
batch.forEach((job, index) => {
|
|
178
|
+
const error = outcomes[index]
|
|
179
|
+
if (error === undefined) return
|
|
180
|
+
// The verb never landed: put the row back and let the caller report it.
|
|
181
|
+
this.sink.apply(job.paths, !job.staged)
|
|
182
|
+
job.reject(error)
|
|
183
|
+
})
|
|
184
|
+
const landed = batch.filter((_, index) => outcomes[index] === undefined)
|
|
185
|
+
if (landed.length > 0) await this.verify(landed)
|
|
186
|
+
for (const job of landed) job.resolve()
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** Drop the pending window (page teardown; the writes already went out). */
|
|
190
|
+
dispose(): void {
|
|
191
|
+
if (this.timer !== undefined) {
|
|
192
|
+
this.clock.clear(this.timer)
|
|
193
|
+
this.timer = undefined
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** Queue one write behind the previous one for the same repository. */
|
|
198
|
+
private enqueue(target: ScmTarget, run: () => Promise<void>): Promise<void> {
|
|
199
|
+
const key = targetKey(target)
|
|
200
|
+
const previous = this.chains.get(key) ?? Promise.resolve()
|
|
201
|
+
// A failed predecessor must not skip this write, hence the same handler on
|
|
202
|
+
// both sides; the stored chain swallows the failure so the next link runs.
|
|
203
|
+
const next = previous.then(run, run)
|
|
204
|
+
this.chains.set(key, next.then(() => undefined, () => undefined))
|
|
205
|
+
return next
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** (Re)start the debounce window. */
|
|
209
|
+
private arm(): void {
|
|
210
|
+
if (this.timer !== undefined) this.clock.clear(this.timer)
|
|
211
|
+
this.timer = this.clock.set(() => {
|
|
212
|
+
this.timer = undefined
|
|
213
|
+
void this.flush()
|
|
214
|
+
}, this.windowMs)
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* One probe per repository over every path the window touched, then correct
|
|
219
|
+
* whatever it contradicts.
|
|
220
|
+
*/
|
|
221
|
+
private async verify(jobs: readonly Job[]): Promise<void> {
|
|
222
|
+
interface Group {
|
|
223
|
+
target: ScmTarget
|
|
224
|
+
paths: string[]
|
|
225
|
+
/** path → the staged-ness the local model now claims (last click wins). */
|
|
226
|
+
staged: Map<string, boolean>
|
|
227
|
+
}
|
|
228
|
+
const groups = new Map<string, Group>()
|
|
229
|
+
for (const job of jobs) {
|
|
230
|
+
const key = targetKey(job.target)
|
|
231
|
+
const group = groups.get(key) ?? { target: job.target, paths: [], staged: new Map<string, boolean>() }
|
|
232
|
+
for (const path of job.paths) {
|
|
233
|
+
if (!group.paths.includes(path)) group.paths.push(path)
|
|
234
|
+
// Two clicks on one path inside the window keep the LAST intent — that
|
|
235
|
+
// is the state the ordered writes left behind.
|
|
236
|
+
group.staged.set(path, job.staged)
|
|
237
|
+
}
|
|
238
|
+
groups.set(key, group)
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
for (const group of groups.values()) {
|
|
242
|
+
let stdout = ''
|
|
243
|
+
try {
|
|
244
|
+
stdout = await this.port.probe(group.target, group.paths)
|
|
245
|
+
} catch {
|
|
246
|
+
// A failed probe is not a failed operation: the writes landed, so the
|
|
247
|
+
// optimistic state stands. Say so rather than silently trusting it.
|
|
248
|
+
this.sink.report({ kind: 'unverified', paths: group.paths })
|
|
249
|
+
continue
|
|
250
|
+
}
|
|
251
|
+
const detail = parseStatusDetail(stdout)
|
|
252
|
+
const wrong: string[] = []
|
|
253
|
+
for (const path of group.paths) {
|
|
254
|
+
const expected = group.staged.get(path) === true
|
|
255
|
+
if (stagedStateOf(detail, path) !== expected) wrong.push(path)
|
|
256
|
+
}
|
|
257
|
+
if (wrong.length === 0) continue
|
|
258
|
+
// Staged-ness is binary, so the opposite of the optimistic value IS what
|
|
259
|
+
// the repository reports: apply the observation, not a guess.
|
|
260
|
+
for (const path of wrong) this.sink.apply([path], !(group.staged.get(path) === true))
|
|
261
|
+
this.sink.report({ kind: 'mismatch', paths: wrong })
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure status model: buckets the detailed porcelain status into the SCM
|
|
3
|
+
* sections and derives the per-path / per-directory marks the file tree
|
|
4
|
+
* paints.
|
|
5
|
+
*
|
|
6
|
+
* Directory marks come from the STATUS PATH SET, never from materialized
|
|
7
|
+
* children: the remote tree is lazy (one SFTP listing per expanded folder),
|
|
8
|
+
* so a collapsed folder holding changes must still light up. Ignored state is
|
|
9
|
+
* inherited downward, because `git status --ignored=matching` reports an
|
|
10
|
+
* ignored directory once, unexpanded.
|
|
11
|
+
*/
|
|
12
|
+
import type { GitPathStatus, GitStatusDetail } from '../../protocol.ts'
|
|
13
|
+
|
|
14
|
+
/** One changed path as the SCM panel shows it. */
|
|
15
|
+
export interface ChangeEntry {
|
|
16
|
+
path: string
|
|
17
|
+
/** Single letter (`M`, `A`, `D`, `R`, `C`, `U`, `T`, `?`). */
|
|
18
|
+
status: string
|
|
19
|
+
/** Rename/copy source. */
|
|
20
|
+
oldPath?: string
|
|
21
|
+
/** True when this entry belongs to the staged (index) half. */
|
|
22
|
+
staged: boolean
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** The SCM sections: conflicts, changes (unstaged + untracked), staged. */
|
|
26
|
+
export interface StatusBuckets {
|
|
27
|
+
conflicts: ChangeEntry[]
|
|
28
|
+
changes: ChangeEntry[]
|
|
29
|
+
staged: ChangeEntry[]
|
|
30
|
+
untracked: ChangeEntry[]
|
|
31
|
+
ignored: string[]
|
|
32
|
+
/** Badge count: staged + unstaged + untracked (conflicts excluded). */
|
|
33
|
+
total: number
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** True for a merge-conflict pair (UU/AA/DD/AU/UA/DU/UD). */
|
|
37
|
+
export function isConflictStatus(x: string, y: string): boolean {
|
|
38
|
+
return x === 'U' || y === 'U' || (x === 'A' && y === 'A') || (x === 'D' && y === 'D')
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** The spelling the rest of this module reads (exported for the row projection). */
|
|
42
|
+
const isConflict = isConflictStatus
|
|
43
|
+
|
|
44
|
+
/** The most descriptive letter for a path that has both halves changed. */
|
|
45
|
+
function pickLetter(staged: string, unstaged: string): string {
|
|
46
|
+
const rank = (letter: string): number => {
|
|
47
|
+
switch (letter) {
|
|
48
|
+
case 'D': return 60
|
|
49
|
+
case 'M': return 50
|
|
50
|
+
case 'T': return 45
|
|
51
|
+
case 'A': return 40
|
|
52
|
+
case 'R': return 30
|
|
53
|
+
case 'C': return 20
|
|
54
|
+
default: return 10
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
if (staged === '' || staged === ' ') return unstaged
|
|
58
|
+
if (unstaged === '' || unstaged === ' ') return staged
|
|
59
|
+
return rank(unstaged) >= rank(staged) ? unstaged : staged
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Bucket the detailed status into the panel's sections. */
|
|
63
|
+
export function bucketStatus(detail: GitStatusDetail): StatusBuckets {
|
|
64
|
+
const conflicts: ChangeEntry[] = []
|
|
65
|
+
const changes: ChangeEntry[] = []
|
|
66
|
+
const staged: ChangeEntry[] = []
|
|
67
|
+
const untracked: ChangeEntry[] = []
|
|
68
|
+
const stagedByPath = new Map<string, string>()
|
|
69
|
+
const worktreeByPath = new Map<string, string>()
|
|
70
|
+
|
|
71
|
+
for (const entry of detail.entries) {
|
|
72
|
+
const { path, x, y, origPath } = entry
|
|
73
|
+
if (x === '?' && y === '?') {
|
|
74
|
+
const item: ChangeEntry = { path, status: '?', staged: false }
|
|
75
|
+
untracked.push(item)
|
|
76
|
+
changes.push(item)
|
|
77
|
+
continue
|
|
78
|
+
}
|
|
79
|
+
if (isConflict(x, y)) {
|
|
80
|
+
conflicts.push({ path, status: 'U', staged: false, ...(origPath === undefined ? {} : { oldPath: origPath }) })
|
|
81
|
+
continue
|
|
82
|
+
}
|
|
83
|
+
if (x !== ' ' && x !== '?') {
|
|
84
|
+
staged.push({ path, status: x, staged: true, ...(origPath === undefined ? {} : { oldPath: origPath }) })
|
|
85
|
+
stagedByPath.set(path, x)
|
|
86
|
+
}
|
|
87
|
+
if (y !== ' ' && y !== '?') {
|
|
88
|
+
changes.push({ path, status: y, staged: false, ...(origPath === undefined ? {} : { oldPath: origPath }) })
|
|
89
|
+
worktreeByPath.set(path, y)
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// The panel lists changes with the worktree letter (VS Code semantics); the
|
|
94
|
+
// tree uses the same precedence so both surfaces agree.
|
|
95
|
+
const merged = changes.map(entry => {
|
|
96
|
+
const stagedLetter = stagedByPath.get(entry.path)
|
|
97
|
+
if (stagedLetter === undefined) return entry
|
|
98
|
+
return { ...entry, status: pickLetter(stagedLetter, entry.status) }
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
return {
|
|
102
|
+
conflicts,
|
|
103
|
+
changes: merged,
|
|
104
|
+
staged,
|
|
105
|
+
untracked,
|
|
106
|
+
ignored: detail.ignored,
|
|
107
|
+
// `untracked` is a SUBSET view of `changes` (an untracked row is pushed to
|
|
108
|
+
// both, see above), so adding it double-counted every untracked file: one
|
|
109
|
+
// untracked file made the rail badge read 2 while the list showed 1. The
|
|
110
|
+
// badge counts entries, and every entry is either staged, changed or
|
|
111
|
+
// conflicted — exactly the three sections the panel renders.
|
|
112
|
+
total: staged.length + merged.length + conflicts.length,
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Whether one porcelain record counts as STAGED for the panel.
|
|
118
|
+
*
|
|
119
|
+
* Mirrors `bucketStatus` exactly, because a verification that disagreed with
|
|
120
|
+
* the list would "correct" a change the user can see was applied: an untracked
|
|
121
|
+
* record is not in the index at all, and a conflict pair lives in its own
|
|
122
|
+
* section (never in "staged changes").
|
|
123
|
+
*/
|
|
124
|
+
export function isStagedStatus(x: string, y: string): boolean {
|
|
125
|
+
if (x === '?' || y === '?') return false
|
|
126
|
+
if (isConflict(x, y)) return false
|
|
127
|
+
return x !== ' '
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* The verb one row's action button issues.
|
|
132
|
+
*
|
|
133
|
+
* The button is the OPPOSITE of the section the row sits in: a row in "staged
|
|
134
|
+
* changes" offers unstage, a row in "changes" offers stage. This exists because
|
|
135
|
+
* the two are trivially confusable — the row already KNOWS which section it is
|
|
136
|
+
* in (`isStaged`), and passing that flag straight through as the verb's target
|
|
137
|
+
* inverts the action, which is silent: the optimistic model has nothing to move
|
|
138
|
+
* (`git add` on an already-staged path, `git restore --staged` on an unstaged
|
|
139
|
+
* one), the probe agrees with the row it never changed, so the click reports
|
|
140
|
+
* neither success nor failure. That is exactly what shipped in 0.8.0 for both
|
|
141
|
+
* directions (stage was a no-op AND unstage was a no-op) while the section-level
|
|
142
|
+
* buttons, which pass a literal, stayed correct.
|
|
143
|
+
*
|
|
144
|
+
* A named verb instead of a boolean, so the call site reads as the action rather
|
|
145
|
+
* than as a state (`applyStage(paths, rowVerb(isStaged))`).
|
|
146
|
+
*/
|
|
147
|
+
export function rowVerb(isStaged: boolean): 'stage' | 'unstage' {
|
|
148
|
+
return isStaged ? 'unstage' : 'stage'
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* The staged-ness of one path in a PATH-LIMITED status probe
|
|
153
|
+
* (`git status --porcelain -z -uno -- <paths>`).
|
|
154
|
+
*
|
|
155
|
+
* The probe runs with `--untracked-files=no`, so a path that is not in the
|
|
156
|
+
* index has no record at all: absence means "not staged" (measured against
|
|
157
|
+
* real git — untracked → no record, staged → `A `/`M `).
|
|
158
|
+
*
|
|
159
|
+
* A trailing slash is an untracked DIRECTORY row (`-u normal` reports the
|
|
160
|
+
* directory once): `git add -- dir/` puts every file inside into the index and
|
|
161
|
+
* the probe then reports those files individually, never `dir/` itself — so a
|
|
162
|
+
* directory path is answered by prefix (also measured).
|
|
163
|
+
*/
|
|
164
|
+
export function stagedStateOf(detail: GitStatusDetail, path: string): boolean {
|
|
165
|
+
if (path.endsWith('/')) {
|
|
166
|
+
return detail.entries.some(entry => entry.path.startsWith(path) && isStagedStatus(entry.x, entry.y))
|
|
167
|
+
}
|
|
168
|
+
const hit = detail.entries.find(entry => entry.path === path || entry.origPath === path)
|
|
169
|
+
return hit === undefined ? false : isStagedStatus(hit.x, hit.y)
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* The local (optimistic) projection of one stage/unstage: what the detailed
|
|
174
|
+
* status looks like the moment the user clicks, before the host answers.
|
|
175
|
+
*
|
|
176
|
+
* The transform mirrors what git does to the two halves of a porcelain record
|
|
177
|
+
* (`git add` clears the worktree half, `git restore --staged` clears the index
|
|
178
|
+
* half) and was checked against real git for the cases that are not obvious:
|
|
179
|
+
* - staging an untracked path (`??`) makes it `A `;
|
|
180
|
+
* - unstaging a staged NEW file returns it to `??` (untracked rows are listed
|
|
181
|
+
* under "changes", so the record stays in the model);
|
|
182
|
+
* - unstaging a staged RENAME leaves HEAD's path deleted in the INDEX — the
|
|
183
|
+
* old path becomes `D ` and the new one untracked (measured, not assumed);
|
|
184
|
+
* - staging a conflict resolves it into the index.
|
|
185
|
+
*
|
|
186
|
+
* Pure, so the panel's instant feedback is assertable without a host: the
|
|
187
|
+
* caller decides WHEN to apply it (and when to apply the inverse).
|
|
188
|
+
*
|
|
189
|
+
* @param detail - the status the panel is showing.
|
|
190
|
+
* @param paths - repo-relative paths that were just staged/unstaged.
|
|
191
|
+
* @param staged - true for a stage, false for an unstage.
|
|
192
|
+
* @returns the projected status (the same object when no path matched).
|
|
193
|
+
*/
|
|
194
|
+
export function optimisticStageDetail(
|
|
195
|
+
detail: GitStatusDetail,
|
|
196
|
+
paths: readonly string[],
|
|
197
|
+
staged: boolean,
|
|
198
|
+
): GitStatusDetail {
|
|
199
|
+
if (paths.length === 0) return detail
|
|
200
|
+
const wanted = new Set(paths)
|
|
201
|
+
const entries: GitPathStatus[] = []
|
|
202
|
+
let touched = false
|
|
203
|
+
for (const entry of detail.entries) {
|
|
204
|
+
if (!wanted.has(entry.path)) {
|
|
205
|
+
entries.push(entry)
|
|
206
|
+
continue
|
|
207
|
+
}
|
|
208
|
+
touched = true
|
|
209
|
+
entries.push(...localStageEntry(entry, staged))
|
|
210
|
+
}
|
|
211
|
+
return touched ? { ...detail, entries } : detail
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/** One record's local projection (a rename unstaged into two records). */
|
|
215
|
+
function localStageEntry(entry: GitPathStatus, staged: boolean): GitPathStatus[] {
|
|
216
|
+
const { path, x, y, origPath } = entry
|
|
217
|
+
const keep = origPath === undefined ? {} : { origPath }
|
|
218
|
+
if (staged) {
|
|
219
|
+
// The index half takes the most descriptive letter it can keep: what is
|
|
220
|
+
// already staged, else what the worktree showed, else a plain add.
|
|
221
|
+
const letter = x !== ' ' && x !== '?' ? x : (y !== ' ' && y !== '?' ? y : 'M')
|
|
222
|
+
return [{ path, x: letter, y: ' ', ...keep }]
|
|
223
|
+
}
|
|
224
|
+
// Unmerged paths: `git restore --staged` refuses them, so the local model
|
|
225
|
+
// shows nothing until the verb reports the failure.
|
|
226
|
+
if (isConflict(x, y)) return [entry]
|
|
227
|
+
// Unstage puts the index half back to HEAD. A path HEAD does not have — an
|
|
228
|
+
// addition, a rename/copy destination, an untracked row — therefore has no
|
|
229
|
+
// index entry left at all and becomes untracked again (measured against real
|
|
230
|
+
// git: unstaging a staged rename leaves the new name untracked and HEAD's old
|
|
231
|
+
// path deleted in the INDEX).
|
|
232
|
+
if (x === '?' || x === 'A' || x === 'R' || x === 'C') {
|
|
233
|
+
const out: GitPathStatus[] = [{ path, x: '?', y: '?' }]
|
|
234
|
+
if (x === 'R' && origPath !== undefined) out.push({ path: origPath, x: 'D', y: ' ' })
|
|
235
|
+
return out
|
|
236
|
+
}
|
|
237
|
+
// HEAD has the path: what is left of the record is the worktree half.
|
|
238
|
+
const worktree = y !== ' ' && y !== '?' ? y : x
|
|
239
|
+
return [{ path, x: ' ', y: worktree, ...keep }]
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Add a path the detailed status does not list yet as a STAGED ADDITION.
|
|
244
|
+
*
|
|
245
|
+
* The panel can stage a file that its own status call never reported: a file
|
|
246
|
+
* inside an expanded untracked DIRECTORY (`-u normal` collapses the directory
|
|
247
|
+
* into one `dir/` row, so the file has no record of its own). Without this the
|
|
248
|
+
* row would simply vanish — staged, yet shown nowhere until the next full
|
|
249
|
+
* refresh. A path that already has a record is left alone.
|
|
250
|
+
*
|
|
251
|
+
* @param detail - the status the panel is showing.
|
|
252
|
+
* @param path - repo-relative path that was just staged.
|
|
253
|
+
* @returns the status with the added record (the same object when it is known).
|
|
254
|
+
*/
|
|
255
|
+
export function insertStagedEntry(detail: GitStatusDetail, path: string): GitStatusDetail {
|
|
256
|
+
if (path === '' || detail.entries.some(entry => entry.path === path || entry.origPath === path)) return detail
|
|
257
|
+
return { ...detail, entries: [...detail.entries, { path, x: 'A', y: ' ' }] }
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/** One local stage/unstage result a full status read may not have seen. */
|
|
261
|
+
export interface StageOverride {
|
|
262
|
+
staged: boolean
|
|
263
|
+
/** Monotonic sequence number of the local change. */
|
|
264
|
+
at: number
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Re-apply the local stage results a FULL status read could not have seen.
|
|
269
|
+
*
|
|
270
|
+
* Skipping the repository-wide refresh after a stage is what makes the click
|
|
271
|
+
* instant, but another verb (a save, a discard, a commit) can still have a full
|
|
272
|
+
* `git status` in flight — and on a large repository that read started minutes
|
|
273
|
+
* ago. Landing it as-is would flip the row the user just staged back into
|
|
274
|
+
* "changes", i.e. the panel would contradict itself for as long as it takes to
|
|
275
|
+
* run the next full read. A read that STARTED before a local change cannot have
|
|
276
|
+
* seen it, so those paths keep the local value; a read that started after them
|
|
277
|
+
* is authoritative and prunes them.
|
|
278
|
+
*
|
|
279
|
+
* @param detail - the freshly read status.
|
|
280
|
+
* @param overrides - repo-relative path → local stage result (one repository).
|
|
281
|
+
* @param startedAt - the sequence number when the read was issued.
|
|
282
|
+
* @returns the detail with the newer local results re-applied.
|
|
283
|
+
*/
|
|
284
|
+
export function applyStageOverrides(
|
|
285
|
+
detail: GitStatusDetail,
|
|
286
|
+
overrides: ReadonlyMap<string, StageOverride>,
|
|
287
|
+
startedAt: number,
|
|
288
|
+
): GitStatusDetail {
|
|
289
|
+
let next = detail
|
|
290
|
+
for (const [path, override] of overrides) {
|
|
291
|
+
if (override.at <= startedAt) continue
|
|
292
|
+
next = optimisticStageDetail(next, [path], override.staged)
|
|
293
|
+
}
|
|
294
|
+
return next
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/** Per-path marks the tree paints: file letter, directory roll-up, ignored. */
|
|
298
|
+
export interface TreeMarks {
|
|
299
|
+
/** Repo-relative path → single status letter (`M`, `A`, `D`, `U`, `?`). */
|
|
300
|
+
files: Map<string, string>
|
|
301
|
+
/** Repo-relative directory → `M` when any descendant changed. */
|
|
302
|
+
dirs: Map<string, string>
|
|
303
|
+
/** Repo-relative paths (files and directories) git ignores. */
|
|
304
|
+
ignored: Set<string>
|
|
305
|
+
/** Directory prefixes implied by the ignored set, for deep paths. */
|
|
306
|
+
ignoredDirs: Set<string>
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/** Strip a trailing slash and a leading `./` so status paths match tree paths. */
|
|
310
|
+
function normalize(path: string): string {
|
|
311
|
+
return path.replace(/^\.\//, '').replace(/\/+$/, '')
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Build tree marks from the detailed status.
|
|
316
|
+
*
|
|
317
|
+
* `buckets` may be supplied when the caller has already bucketed the same
|
|
318
|
+
* status. The marks need the same four lists, and recomputing them meant every
|
|
319
|
+
* store publish walked the whole change list twice.
|
|
320
|
+
*/
|
|
321
|
+
export function buildTreeMarks(detail: GitStatusDetail, buckets: StatusBuckets = bucketStatus(detail)): TreeMarks {
|
|
322
|
+
const files = new Map<string, string>()
|
|
323
|
+
const dirs = new Map<string, string>()
|
|
324
|
+
const stagedLetters = new Map<string, string>()
|
|
325
|
+
const worktreeLetters = new Map<string, string>()
|
|
326
|
+
|
|
327
|
+
const put = (rawPath: string, letter: string): void => {
|
|
328
|
+
const path = normalize(rawPath)
|
|
329
|
+
if (path === '') return
|
|
330
|
+
// `-u normal` reports an untracked DIRECTORY as one `dir/` record: strip
|
|
331
|
+
// the slash and it becomes a directory mark, not a phantom file.
|
|
332
|
+
if (rawPath.endsWith('/')) dirs.set(path, letter === '?' ? 'M' : letter)
|
|
333
|
+
else files.set(path, letter)
|
|
334
|
+
}
|
|
335
|
+
for (const entry of buckets.staged) stagedLetters.set(normalize(entry.path), entry.status)
|
|
336
|
+
for (const entry of buckets.changes) worktreeLetters.set(normalize(entry.path), entry.status)
|
|
337
|
+
for (const entry of buckets.untracked) put(entry.path, '?')
|
|
338
|
+
for (const entry of buckets.conflicts) put(entry.path, 'U')
|
|
339
|
+
|
|
340
|
+
const all = new Set<string>([...stagedLetters.keys(), ...worktreeLetters.keys()])
|
|
341
|
+
for (const path of all) {
|
|
342
|
+
const stagedLetter = stagedLetters.get(path)
|
|
343
|
+
const worktreeLetter = worktreeLetters.get(path)
|
|
344
|
+
if (files.has(path) || dirs.has(path)) continue
|
|
345
|
+
files.set(path, stagedLetter === undefined
|
|
346
|
+
? (worktreeLetter as string)
|
|
347
|
+
: worktreeLetter === undefined
|
|
348
|
+
? stagedLetter
|
|
349
|
+
: pickLetter(stagedLetter, worktreeLetter))
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// Every directory on the way to a changed path is itself changed.
|
|
353
|
+
const markParents = (path: string): void => {
|
|
354
|
+
const segments = path.split('/')
|
|
355
|
+
let acc = ''
|
|
356
|
+
for (let i = 0; i < segments.length - 1; i++) {
|
|
357
|
+
acc = acc === '' ? (segments[i] as string) : `${acc}/${segments[i]}`
|
|
358
|
+
if (dirs.get(acc) === 'M') continue
|
|
359
|
+
dirs.set(acc, 'M')
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
for (const path of [...files.keys()]) markParents(path)
|
|
363
|
+
for (const path of [...dirs.keys()]) markParents(path)
|
|
364
|
+
|
|
365
|
+
const ignored = new Set<string>()
|
|
366
|
+
const ignoredDirs = new Set<string>()
|
|
367
|
+
for (const raw of buckets.ignored) {
|
|
368
|
+
const path = normalize(raw)
|
|
369
|
+
if (path === '') continue
|
|
370
|
+
if (raw.endsWith('/')) ignoredDirs.add(path)
|
|
371
|
+
else ignored.add(path)
|
|
372
|
+
const segments = path.split('/')
|
|
373
|
+
let acc = ''
|
|
374
|
+
for (let i = 0; i < segments.length - 1; i++) {
|
|
375
|
+
acc = acc === '' ? (segments[i] as string) : `${acc}/${segments[i]}`
|
|
376
|
+
ignoredDirs.add(acc)
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
return { files, dirs, ignored, ignoredDirs }
|
|
381
|
+
}
|