@gitkraken/core-gitlens 0.16.5 → 0.18.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/CHANGELOG.md +35 -1
- package/dist/git/cache.d.ts +15 -2
- package/dist/git/cache.js +36 -9
- package/dist/git/cache.js.map +1 -1
- package/dist/git/models/repository.d.ts +4 -2
- package/dist/git/models/repository.js +1 -0
- package/dist/git/models/repository.js.map +1 -1
- package/dist/git/models/worktree.d.ts +5 -1
- package/dist/git/models/worktree.js +5 -3
- package/dist/git/models/worktree.js.map +1 -1
- package/dist/git/providers/provider.d.ts +17 -0
- package/dist/git/providers/worktrees.d.ts +17 -2
- package/dist/git/run.types.d.ts +7 -0
- package/dist/git/service.d.ts +5 -2
- package/dist/git/service.js +11 -5
- package/dist/git/service.js.map +1 -1
- package/dist/git/watching/classifyChange.d.ts +4 -3
- package/dist/git/watching/classifyChange.js +7 -4
- package/dist/git/watching/classifyChange.js.map +1 -1
- package/dist/git/watching/gitIgnoreFilter.d.ts +21 -3
- package/dist/git/watching/gitIgnoreFilter.js +74 -14
- package/dist/git/watching/gitIgnoreFilter.js.map +1 -1
- package/dist/git/watching/provider.d.ts +14 -0
- package/dist/git/watching/watchGroup.d.ts +142 -3
- package/dist/git/watching/watchGroup.js +424 -19
- package/dist/git/watching/watchGroup.js.map +1 -1
- package/dist/git/watching/watchService.d.ts +39 -1
- package/dist/git/watching/watchService.js +81 -20
- package/dist/git/watching/watchService.js.map +1 -1
- package/dist/git/watching/watchSession.d.ts +13 -8
- package/dist/git/watching/watchSession.js +50 -10
- package/dist/git/watching/watchSession.js.map +1 -1
- package/dist/git/watching/watcherPatterns.d.ts +14 -0
- package/dist/git/watching/watcherPatterns.js +38 -1
- package/dist/git/watching/watcherPatterns.js.map +1 -1
- package/dist/git/watching/worktreeSummary.d.ts +97 -7
- package/dist/git/watching/worktreeSummary.js +454 -27
- package/dist/git/watching/worktreeSummary.js.map +1 -1
- package/dist/git-cli/cliGitProvider.d.ts +26 -0
- package/dist/git-cli/cliGitProvider.js +70 -9
- package/dist/git-cli/cliGitProvider.js.map +1 -1
- package/dist/git-cli/exec/commandClassifier.d.ts +9 -3
- package/dist/git-cli/exec/commandClassifier.js +98 -17
- package/dist/git-cli/exec/commandClassifier.js.map +1 -1
- package/dist/git-cli/exec/git.d.ts +20 -0
- package/dist/git-cli/exec/git.js +69 -7
- package/dist/git-cli/exec/git.js.map +1 -1
- package/dist/git-cli/exec/headState.d.ts +5 -0
- package/dist/git-cli/exec/headState.js +14 -1
- package/dist/git-cli/exec/headState.js.map +1 -1
- package/dist/git-cli/parsers/worktreeParser.js +2 -2
- package/dist/git-cli/parsers/worktreeParser.js.map +1 -1
- package/dist/git-cli/providers/branches.js +9 -36
- package/dist/git-cli/providers/branches.js.map +1 -1
- package/dist/git-cli/providers/config.d.ts +21 -0
- package/dist/git-cli/providers/config.js +72 -1
- package/dist/git-cli/providers/config.js.map +1 -1
- package/dist/git-cli/providers/graph.js +1 -1
- package/dist/git-cli/providers/graph.js.map +1 -1
- package/dist/git-cli/providers/operations.d.ts +3 -0
- package/dist/git-cli/providers/operations.js +14 -5
- package/dist/git-cli/providers/operations.js.map +1 -1
- package/dist/git-cli/providers/worktrees.d.ts +25 -3
- package/dist/git-cli/providers/worktrees.js +125 -32
- package/dist/git-cli/providers/worktrees.js.map +1 -1
- package/dist/git-cli/service.d.ts +2 -0
- package/dist/git-cli/service.js +4 -2
- package/dist/git-cli/service.js.map +1 -1
- package/dist/plus/integrations/providers/issueSorts.d.ts +0 -1
- package/dist/plus/integrations/reads/drains.d.ts +0 -1
- package/package.json +1 -1
|
@@ -3,7 +3,9 @@ import { CancellationError, isCancellationError, raceWithSignal } from "../../ut
|
|
|
3
3
|
import { Logger as _logger } from "../../utils/logger.js";
|
|
4
4
|
import { Emitter } from "../../utils/event.js";
|
|
5
5
|
import { getRepositoryKey } from "../../utils/uri.js";
|
|
6
|
+
import { joinPaths } from "../../utils/path.js";
|
|
6
7
|
import { getRefChangeKind } from "./changeEvent.js";
|
|
8
|
+
import { LruMap } from "../../utils/lruMap.js";
|
|
7
9
|
//#region ../git/src/watching/worktreeSummary.ts
|
|
8
10
|
/** Read groups, as bit flags so a pending set is a number rather than an allocation per event */
|
|
9
11
|
const dirtyField = 1;
|
|
@@ -22,12 +24,33 @@ const baseMoveField = 32;
|
|
|
22
24
|
const allFields = 7;
|
|
23
25
|
const maxConcurrentReadsPerRepository = 4;
|
|
24
26
|
const maxConcurrentReads = 8;
|
|
27
|
+
const maxUnwatchedReads = 256;
|
|
28
|
+
const maxDeferredDirtyMs = 1e4;
|
|
25
29
|
const emptyRefs = Object.freeze([]);
|
|
26
30
|
const noBaseKeys = [
|
|
27
31
|
void 0,
|
|
28
32
|
void 0,
|
|
29
33
|
void 0
|
|
30
34
|
];
|
|
35
|
+
/** A read of a worktree without a subscription, which concurrent callers share */
|
|
36
|
+
var UnwatchedFlight = class {
|
|
37
|
+
key;
|
|
38
|
+
probeDirty;
|
|
39
|
+
disposed;
|
|
40
|
+
abort = new AbortController();
|
|
41
|
+
/** Callers waiting for it; the read is aborted once none is */
|
|
42
|
+
callers = 0;
|
|
43
|
+
/** Read groups made due while it ran, carried into what it stores */
|
|
44
|
+
due = 0;
|
|
45
|
+
promise;
|
|
46
|
+
constructor(key, probeDirty, disposed, read) {
|
|
47
|
+
this.key = key;
|
|
48
|
+
this.probeDirty = probeDirty;
|
|
49
|
+
this.disposed = disposed;
|
|
50
|
+
this.promise = Promise.resolve().then(() => read(this));
|
|
51
|
+
this.promise.catch(noop);
|
|
52
|
+
}
|
|
53
|
+
};
|
|
31
54
|
var SummarySubscription = class {
|
|
32
55
|
entry;
|
|
33
56
|
options;
|
|
@@ -44,6 +67,9 @@ var SummarySubscription = class {
|
|
|
44
67
|
get current() {
|
|
45
68
|
return this.disposed ? void 0 : this.entry.value;
|
|
46
69
|
}
|
|
70
|
+
get dirtyReadAt() {
|
|
71
|
+
return this.disposed || this.entry.value == null ? void 0 : this.entry.dirtyReadAt;
|
|
72
|
+
}
|
|
47
73
|
get onDidChange() {
|
|
48
74
|
return this.emitter.event;
|
|
49
75
|
}
|
|
@@ -79,7 +105,10 @@ var SummarySubscription = class {
|
|
|
79
105
|
* has working changes, and how far it is past its base — kept current by the repository watch layer.
|
|
80
106
|
*
|
|
81
107
|
* **Lifecycle.** Entries are per repository key and ref-counted across {@link subscribe} calls. The first subscriber
|
|
82
|
-
* starts watching the worktree and reads everything
|
|
108
|
+
* starts watching the worktree and reads everything, or, for a worktree last read without a subscription, starts from
|
|
109
|
+
* that read: it re-reads the groups whose inputs moved since, by the same rules as such a read, and always `dirty` —
|
|
110
|
+
* unless its working-tree watch is still starting, when the rescan that watch owes reads it, and the summary keeps the
|
|
111
|
+
* saved read's flag, with its `dirtyReadAt`, until then. The last one to leave aborts any in-flight read, releases the
|
|
83
112
|
* watch and drops the value. Each subscriber's timing options join the watch session's cadence (the shortest wins);
|
|
84
113
|
* the service's own listening subscriptions never constrain it.
|
|
85
114
|
*
|
|
@@ -106,11 +135,27 @@ var SummarySubscription = class {
|
|
|
106
135
|
* **Value-change-only notification.** {@link WorktreeSummarySubscription.onDidChange} and {@link onDidChangeSummary}
|
|
107
136
|
* fire only when some field's value differs from the previous summary — a recompute that reads the same answer is
|
|
108
137
|
* silent, so a consumer can re-render on every event without diffing. A failed recompute keeps the last values and
|
|
109
|
-
* sets `stale` (firing once); the next successful one clears it.
|
|
138
|
+
* sets `stale` (firing once); the next successful one clears it. A summary still showing the dirty flag of a read
|
|
139
|
+
* without a subscription fires again, unchanged, once that flag is read again (its `dirtyReadAt` cleared).
|
|
140
|
+
*
|
|
141
|
+
* **Worktrees without a subscription.** {@link getSummary} and {@link getSummarySnapshot} answer one from its last read
|
|
142
|
+
* (kept for the {@link maxUnwatchedReads} most recently asked), re-reading only the groups whose inputs moved since:
|
|
143
|
+
* HEAD's branch or commit reads all of them; the upstream's tip, or its name, reads `branch`; a ref the base depends on
|
|
144
|
+
* (where a remote's `HEAD` symref points, for one) reads `base`, re-measured from the previous one where that can
|
|
145
|
+
* answer; the config inputs its base read consulted read `branch` and the base when they changed; and the worktree's
|
|
146
|
+
* index file reads `dirty`. The inputs come from core's caches, so nothing moved means no git at all; only a `.git`
|
|
147
|
+
* watch of the worktree (or core's own writes) keeps them current, so a host must watch the `.git` of a worktree it
|
|
148
|
+
* asks about this way for others' changes to show. Its working tree is unwatched, so an edit that leaves the index
|
|
149
|
+
* alone goes unseen until `dirty` is read again: the snapshot says when that was, and `probeDirty` reads it anyway.
|
|
150
|
+
* Concurrent reads of a worktree share one, and take no read slot: each is a caller's foreground read, which never
|
|
151
|
+
* waits behind subscribed worktrees' runs. A first subscriber takes over the last read.
|
|
110
152
|
*/
|
|
111
153
|
var WorktreeSummaryService = class {
|
|
112
154
|
_onDidChangeSummary = new Emitter();
|
|
113
|
-
/**
|
|
155
|
+
/**
|
|
156
|
+
* Fires when the summary of any subscribed worktree changes value, and with the same summary when a dirty flag kept
|
|
157
|
+
* from a read without a subscription is read again (see {@link WorktreeSummarySubscription.dirtyReadAt})
|
|
158
|
+
*/
|
|
114
159
|
get onDidChangeSummary() {
|
|
115
160
|
return this._onDidChangeSummary.event;
|
|
116
161
|
}
|
|
@@ -120,9 +165,14 @@ var WorktreeSummaryService = class {
|
|
|
120
165
|
activeReads = 0;
|
|
121
166
|
/** Entries waiting for a read slot, oldest first */
|
|
122
167
|
readWaiters = /* @__PURE__ */ new Map();
|
|
168
|
+
/** The last read of each worktree without a subscription, by repository key */
|
|
169
|
+
unwatchedReads = new LruMap(maxUnwatchedReads);
|
|
170
|
+
/** The read in flight for each worktree without a subscription, by repository key */
|
|
171
|
+
unwatchedFlights = /* @__PURE__ */ new Map();
|
|
123
172
|
watchService;
|
|
124
173
|
getRepository;
|
|
125
174
|
getCommonPath;
|
|
175
|
+
stat;
|
|
126
176
|
verifyBaseRemeasure;
|
|
127
177
|
mergeTargetsListener;
|
|
128
178
|
_disposed = false;
|
|
@@ -130,6 +180,7 @@ var WorktreeSummaryService = class {
|
|
|
130
180
|
this.watchService = options.watchService;
|
|
131
181
|
this.getRepository = options.getRepository;
|
|
132
182
|
this.getCommonPath = options.getCommonPath;
|
|
183
|
+
this.stat = options.stat;
|
|
133
184
|
this.verifyBaseRemeasure = options.verifyBaseRemeasure;
|
|
134
185
|
this.mergeTargetsListener = options.onDidChangeMergeTargets?.((e) => this.onMergeTargetsChanged(e));
|
|
135
186
|
}
|
|
@@ -141,12 +192,22 @@ var WorktreeSummaryService = class {
|
|
|
141
192
|
this._disposed = true;
|
|
142
193
|
for (const resolve of this.readWaiters.values()) resolve(false);
|
|
143
194
|
this.readWaiters.clear();
|
|
195
|
+
for (const flight of this.unwatchedFlights.values()) {
|
|
196
|
+
flight.disposed = true;
|
|
197
|
+
flight.abort.abort();
|
|
198
|
+
}
|
|
199
|
+
this.unwatchedFlights.clear();
|
|
200
|
+
this.unwatchedReads.clear();
|
|
144
201
|
this.mergeTargetsListener?.dispose();
|
|
145
202
|
for (const entry of [...this.entries.values()]) for (const subscription of [...entry.subscribers]) subscription.dispose();
|
|
146
203
|
this.entries.clear();
|
|
147
204
|
this._onDidChangeSummary.dispose();
|
|
148
205
|
}
|
|
149
|
-
/**
|
|
206
|
+
/**
|
|
207
|
+
* Subscribes to a worktree's summary, starting to watch and read it if this is its first subscriber: from its last
|
|
208
|
+
* read without a subscription, if any, re-reading the groups whose inputs moved since and the dirty flag — at once,
|
|
209
|
+
* or, while its working-tree watch is still starting, as that watch goes live
|
|
210
|
+
*/
|
|
150
211
|
subscribe(repoPath, options) {
|
|
151
212
|
const key = getRepositoryKey(repoPath);
|
|
152
213
|
let entry = this._disposed ? void 0 : this.entries.get(key);
|
|
@@ -161,33 +222,179 @@ var WorktreeSummaryService = class {
|
|
|
161
222
|
target.subscribers.add(subscription);
|
|
162
223
|
if (isNew) {
|
|
163
224
|
if (!target.disposed) {
|
|
164
|
-
|
|
225
|
+
this.unwatchedFlights.delete(key);
|
|
165
226
|
target.running = this.start(target);
|
|
166
227
|
}
|
|
167
228
|
} else if (target.handle != null) subscription.attach(target.handle.session);
|
|
168
|
-
else if (target.running == null && !target.disposed)
|
|
169
|
-
target.pending = 23;
|
|
170
|
-
target.running = this.start(target);
|
|
171
|
-
}
|
|
229
|
+
else if (target.running == null && !target.disposed) target.running = this.start(target);
|
|
172
230
|
return subscription;
|
|
173
231
|
}
|
|
174
232
|
/**
|
|
175
233
|
* Returns a worktree's summary. For a subscribed worktree, the cached value — after waiting for any run in flight
|
|
176
|
-
* or pending; it rejects with the read's error when there is still no value. Otherwise
|
|
177
|
-
*
|
|
234
|
+
* or pending; it rejects with the read's error when there is still no value. Otherwise the last read of it,
|
|
235
|
+
* re-reading what moved since (see {@link getSummarySnapshot}).
|
|
178
236
|
*/
|
|
179
237
|
async getSummary(repoPath, cancellation) {
|
|
180
238
|
const key = getRepositoryKey(repoPath);
|
|
181
239
|
const entry = this.entries.get(key);
|
|
182
|
-
if (entry == null)
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
240
|
+
if (entry == null) return (await this.getUnwatchedSnapshot(key, false, cancellation)).summary;
|
|
241
|
+
return this.getSubscribedSummary(entry, cancellation);
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* Returns a worktree's summary, with when its dirty flag was read. A subscribed worktree answers as
|
|
245
|
+
* {@link getSummary} does, with `dirtyReadAt` `undefined` once its summary follows its working tree, and until then
|
|
246
|
+
* the saved read's (see {@link WorktreeSummarySubscription.dirtyReadAt}), which `probeDirty` reads again. Any other
|
|
247
|
+
* answers from its last read, re-reading only the groups whose inputs moved since (no git when none did), and in
|
|
248
|
+
* full the first time, or after a failed or stale read. Its working tree is unwatched, so the dirty flag is re-read
|
|
249
|
+
* only when the index file moved, or with `probeDirty`; a rejection drops the last read. Its inputs are core's
|
|
250
|
+
* cached answers, which only a `.git` watch of the worktree (or core's own writes) keeps current.
|
|
251
|
+
*/
|
|
252
|
+
async getSummarySnapshot(repoPath, options) {
|
|
253
|
+
const key = getRepositoryKey(repoPath);
|
|
254
|
+
const entry = this.entries.get(key);
|
|
255
|
+
if (entry == null) return this.getUnwatchedSnapshot(key, options?.probeDirty ?? false, options?.cancellation);
|
|
256
|
+
return this.getSubscribedSnapshot(entry, options?.probeDirty ?? false, options?.cancellation);
|
|
257
|
+
}
|
|
258
|
+
async getSubscribedSnapshot(entry, probeDirty, cancellation) {
|
|
259
|
+
let summary = await this.getSubscribedSummary(entry, cancellation);
|
|
260
|
+
if (probeDirty && entry.dirtyReadAt != null) {
|
|
261
|
+
this.queue(entry, dirtyField);
|
|
262
|
+
summary = await this.getSubscribedSummary(entry, cancellation);
|
|
186
263
|
}
|
|
264
|
+
return {
|
|
265
|
+
summary,
|
|
266
|
+
dirtyReadAt: entry.dirtyReadAt
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
async getSubscribedSummary(entry, cancellation) {
|
|
187
270
|
if (entry.running != null) await (cancellation != null ? raceWithSignal(entry.running, cancellation) : entry.running);
|
|
188
271
|
if (entry.disposed) throw new CancellationError();
|
|
189
272
|
if (entry.value != null) return entry.value;
|
|
190
|
-
throw entry.error ?? /* @__PURE__ */ new Error(`No summary available for '${key}'`);
|
|
273
|
+
throw entry.error ?? /* @__PURE__ */ new Error(`No summary available for '${entry.key}'`);
|
|
274
|
+
}
|
|
275
|
+
/** Joins the read in flight for `key`, or starts one; the read is aborted only once no caller waits for it */
|
|
276
|
+
async getUnwatchedSnapshot(key, probeDirty, cancellation) {
|
|
277
|
+
if (cancellation?.aborted) throw new CancellationError();
|
|
278
|
+
let flight = this.getJoinableFlight(key);
|
|
279
|
+
if (probeDirty) {
|
|
280
|
+
while (flight != null && !flight.probeDirty) {
|
|
281
|
+
const waited = flight;
|
|
282
|
+
try {
|
|
283
|
+
await (cancellation != null ? raceWithSignal(waited.promise, cancellation) : waited.promise);
|
|
284
|
+
} catch (ex) {
|
|
285
|
+
if (cancellation?.aborted) throw ex;
|
|
286
|
+
}
|
|
287
|
+
flight = this.getJoinableFlight(key);
|
|
288
|
+
if (flight === waited) flight = void 0;
|
|
289
|
+
}
|
|
290
|
+
const entry = this.entries.get(key);
|
|
291
|
+
if (entry != null) return this.getSubscribedSnapshot(entry, true, cancellation);
|
|
292
|
+
}
|
|
293
|
+
if (flight == null) {
|
|
294
|
+
flight = new UnwatchedFlight(key, probeDirty, this._disposed, (f) => this.readUnwatched(f));
|
|
295
|
+
if (!this._disposed) this.unwatchedFlights.set(key, flight);
|
|
296
|
+
}
|
|
297
|
+
const current = flight;
|
|
298
|
+
current.callers++;
|
|
299
|
+
try {
|
|
300
|
+
const read = await (cancellation != null ? raceWithSignal(current.promise, cancellation) : current.promise);
|
|
301
|
+
return {
|
|
302
|
+
summary: read.result.summary,
|
|
303
|
+
dirtyReadAt: read.dirtyReadAt
|
|
304
|
+
};
|
|
305
|
+
} finally {
|
|
306
|
+
current.callers--;
|
|
307
|
+
if (current.callers === 0) {
|
|
308
|
+
if (this.unwatchedFlights.get(key) === current) this.unwatchedFlights.delete(key);
|
|
309
|
+
current.disposed = true;
|
|
310
|
+
current.abort.abort();
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
/**
|
|
315
|
+
* The read in flight for `key`, unless core wrote a merge target or base while it ran: it may have read the stored
|
|
316
|
+
* values before the write, and a caller after an awaited write sees them, as one of a subscribed worktree does
|
|
317
|
+
*/
|
|
318
|
+
getJoinableFlight(key) {
|
|
319
|
+
const flight = this.unwatchedFlights.get(key);
|
|
320
|
+
return flight?.due === 0 ? flight : void 0;
|
|
321
|
+
}
|
|
322
|
+
async readUnwatched(flight) {
|
|
323
|
+
const { key } = flight;
|
|
324
|
+
const signal = flight.abort.signal;
|
|
325
|
+
if (flight.disposed) throw new CancellationError();
|
|
326
|
+
let last = this.unwatchedReads.get(key);
|
|
327
|
+
if (last?.result.summary.stale) last = void 0;
|
|
328
|
+
try {
|
|
329
|
+
const repo = this.getRepository(key);
|
|
330
|
+
if (repo == null) throw new Error(`No repository found for '${key}'`);
|
|
331
|
+
const headRemotes = last != null ? getHeadRemotes(last.result.summary.baseDependsOn) : void 0;
|
|
332
|
+
const probe = await this.probeUnwatched(repo, headRemotes, true, signal);
|
|
333
|
+
if (flight.disposed) throw new CancellationError();
|
|
334
|
+
let fields;
|
|
335
|
+
let baseMoves;
|
|
336
|
+
if (last == null) fields = 23;
|
|
337
|
+
else {
|
|
338
|
+
({fields, baseMoves} = await getMovedFields(repo, last, probe, flight.probeDirty, signal));
|
|
339
|
+
if (flight.disposed) throw new CancellationError();
|
|
340
|
+
if (fields === 0) {
|
|
341
|
+
this.unwatchedReads.touch(key);
|
|
342
|
+
return last;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
if (headRemotes != null && (fields & 60) !== 0) {
|
|
346
|
+
await readRemoteHeads(repo, probe.remoteHeads, void 0, signal);
|
|
347
|
+
if (flight.disposed) throw new CancellationError();
|
|
348
|
+
}
|
|
349
|
+
let result = await readSummary(repo, key, fields, last?.result.summary, last?.result.baseKeys, last?.result.baseConfigKey, last?.result.baseDefaultBranch, baseMoves, signal, this.verifyBaseRemeasure);
|
|
350
|
+
const kept = last?.result.summary;
|
|
351
|
+
if (kept != null && summariesEqual(kept, result.summary)) result = {
|
|
352
|
+
...result,
|
|
353
|
+
summary: kept
|
|
354
|
+
};
|
|
355
|
+
const read = {
|
|
356
|
+
result,
|
|
357
|
+
dirtyReadAt: result.dirtyReadAt ?? last?.dirtyReadAt ?? Date.now(),
|
|
358
|
+
inputs: toUnwatchedInputs(probe, result.summary),
|
|
359
|
+
due: flight.due
|
|
360
|
+
};
|
|
361
|
+
if (this.unwatchedFlights.get(key) === flight && !this.entries.has(key)) this.unwatchedReads.set(key, read);
|
|
362
|
+
return read;
|
|
363
|
+
} catch (ex) {
|
|
364
|
+
if (!isCancellationError(ex) && this.unwatchedFlights.get(key) === flight) this.unwatchedReads.delete(key);
|
|
365
|
+
throw ex;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
/**
|
|
369
|
+
* Reads an unwatched read's inputs: cached answers but for the index file's stat (see {@link UnwatchedInputs}), with
|
|
370
|
+
* the `HEAD` symrefs of `remotes` (every remote's when `undefined`); without `statIndex`, the index is unknown
|
|
371
|
+
*/
|
|
372
|
+
async probeUnwatched(repo, remotes, statIndex, signal) {
|
|
373
|
+
const remoteHeads = /* @__PURE__ */ new Map();
|
|
374
|
+
const stat = statIndex ? this.stat : void 0;
|
|
375
|
+
const [current, tips, gitDir] = await Promise.all([
|
|
376
|
+
repo.branches.getBranch(void 0, void 0, signal),
|
|
377
|
+
repo.refs.getRefTips(void 0, signal),
|
|
378
|
+
stat != null ? getGitDir(repo) : void 0,
|
|
379
|
+
readRemoteHeads(repo, remoteHeads, remotes, signal)
|
|
380
|
+
]);
|
|
381
|
+
let index;
|
|
382
|
+
if (stat != null && gitDir != null) try {
|
|
383
|
+
const stats = await stat(joinPaths(gitDir.uri.fsPath, "index"));
|
|
384
|
+
index = stats != null ? `${stats.size}:${stats.mtime}` : "";
|
|
385
|
+
} catch {
|
|
386
|
+
index = void 0;
|
|
387
|
+
}
|
|
388
|
+
const detached = current?.detached ?? false;
|
|
389
|
+
return {
|
|
390
|
+
branch: detached ? void 0 : current?.name || void 0,
|
|
391
|
+
detached,
|
|
392
|
+
sha: current?.sha,
|
|
393
|
+
upstream: current?.upstream?.name,
|
|
394
|
+
tips: getTipsByName(tips),
|
|
395
|
+
remoteHeads,
|
|
396
|
+
index
|
|
397
|
+
};
|
|
191
398
|
}
|
|
192
399
|
getResourceUsage() {
|
|
193
400
|
return { "worktreeSummary.entries.count": this.entries.size };
|
|
@@ -209,6 +416,15 @@ var WorktreeSummaryService = class {
|
|
|
209
416
|
this.fail(entry, /* @__PURE__ */ new Error(`Unable to watch '${entry.key}': no repository or git directory`));
|
|
210
417
|
return;
|
|
211
418
|
}
|
|
419
|
+
let last = this.unwatchedReads.get(entry.key);
|
|
420
|
+
this.unwatchedReads.delete(entry.key);
|
|
421
|
+
if (last?.result.summary.stale) last = void 0;
|
|
422
|
+
if (last != null) {
|
|
423
|
+
entry.seed = last.result.summary;
|
|
424
|
+
entry.baseKeys = last.result.baseKeys;
|
|
425
|
+
entry.baseConfigKey = last.result.baseConfigKey;
|
|
426
|
+
entry.baseDefaultBranch = last.result.baseDefaultBranch;
|
|
427
|
+
} else entry.pending |= 23;
|
|
212
428
|
entry.handle = handle;
|
|
213
429
|
entry.slotsKey = getRepositoryKey(gitDir.commonUri ?? gitDir.uri);
|
|
214
430
|
const session = handle.session;
|
|
@@ -227,10 +443,56 @@ var WorktreeSummaryService = class {
|
|
|
227
443
|
workingTreeSub
|
|
228
444
|
];
|
|
229
445
|
for (const subscription of entry.subscribers) subscription.attach(session);
|
|
446
|
+
if (last != null) {
|
|
447
|
+
await this.queueSeedStart(entry, last, this.watchService.isWorkingTreeStarting(entry.key));
|
|
448
|
+
if (entry.disposed) return;
|
|
449
|
+
}
|
|
230
450
|
await this.drain(entry);
|
|
231
451
|
}
|
|
452
|
+
/**
|
|
453
|
+
* Queues the groups of the saved read `last` whose inputs moved since, as an unwatched read finds them, and the
|
|
454
|
+
* dirty flag, which no input vouches for; everything when its inputs can't be read. With `deferDirty`, the dirty
|
|
455
|
+
* flag is left to the rescan of a working-tree watch still starting, and the value keeps `last`'s until then
|
|
456
|
+
*/
|
|
457
|
+
async queueSeedStart(entry, last, deferDirty) {
|
|
458
|
+
const signal = entry.abort.signal;
|
|
459
|
+
let moved;
|
|
460
|
+
try {
|
|
461
|
+
const repo = this.getRepository(entry.key);
|
|
462
|
+
if (repo != null) {
|
|
463
|
+
const probe = await this.probeUnwatched(repo, getHeadRemotes(last.result.summary.baseDependsOn), false, signal);
|
|
464
|
+
if (entry.disposed) return;
|
|
465
|
+
moved = await getMovedFields(repo, last, probe, true, signal);
|
|
466
|
+
}
|
|
467
|
+
} catch {}
|
|
468
|
+
if (entry.disposed) return;
|
|
469
|
+
if (moved == null) {
|
|
470
|
+
dropSeed(entry);
|
|
471
|
+
entry.pending |= 23;
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
474
|
+
if (deferDirty) {
|
|
475
|
+
entry.pending |= moved.fields & -2;
|
|
476
|
+
entry.dirtyReadAt = last.dirtyReadAt;
|
|
477
|
+
entry.dirtyReadTimer = setTimeout(() => {
|
|
478
|
+
entry.dirtyReadTimer = void 0;
|
|
479
|
+
this.queue(entry, dirtyField);
|
|
480
|
+
}, maxDeferredDirtyMs);
|
|
481
|
+
if (entry.pending === 0 && entry.seed != null) {
|
|
482
|
+
const seed = entry.seed;
|
|
483
|
+
entry.error = void 0;
|
|
484
|
+
entry.seed = void 0;
|
|
485
|
+
entry.value = seed;
|
|
486
|
+
this.fireChange(entry, seed);
|
|
487
|
+
}
|
|
488
|
+
} else entry.pending |= moved.fields;
|
|
489
|
+
if (moved.baseMoves != null) {
|
|
490
|
+
entry.baseMoves ??= /* @__PURE__ */ new Set();
|
|
491
|
+
for (const ref of moved.baseMoves) entry.baseMoves.add(ref);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
232
494
|
onRepoChanged(entry, e) {
|
|
233
|
-
const value = entry.value;
|
|
495
|
+
const value = entry.value ?? entry.seed;
|
|
234
496
|
if (value == null || e.changed("unknown", "head")) {
|
|
235
497
|
this.queue(entry, allFields | (e.changed("config") ? baseConfigField : 0));
|
|
236
498
|
return;
|
|
@@ -267,14 +529,28 @@ var WorktreeSummaryService = class {
|
|
|
267
529
|
if (entry.handle == null) continue;
|
|
268
530
|
const value = entry.value;
|
|
269
531
|
if (value != null && (value.branch == null || e.refs != null && !e.refs.has(value.branch))) continue;
|
|
270
|
-
|
|
271
|
-
if (commonPath != null && commonPath !== e.repoPath) continue;
|
|
532
|
+
if (!this.isInRepository(entry.key, e.repoPath)) continue;
|
|
272
533
|
this.queue(entry, baseKeysField);
|
|
273
534
|
}
|
|
535
|
+
for (const read of this.unwatchedReads.values()) {
|
|
536
|
+
const value = read.result.summary;
|
|
537
|
+
if (value.branch == null || e.refs != null && !e.refs.has(value.branch)) continue;
|
|
538
|
+
if (!this.isInRepository(value.repoPath, e.repoPath)) continue;
|
|
539
|
+
read.due |= baseKeysField;
|
|
540
|
+
}
|
|
541
|
+
for (const flight of this.unwatchedFlights.values()) {
|
|
542
|
+
if (!this.isInRepository(flight.key, e.repoPath)) continue;
|
|
543
|
+
flight.due |= baseKeysField;
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
isInRepository(key, commonPath) {
|
|
547
|
+
const path = this.getCommonPath?.(key);
|
|
548
|
+
return path == null || path === commonPath;
|
|
274
549
|
}
|
|
275
550
|
queue(entry, fields, baseMoves) {
|
|
276
551
|
if (entry.disposed || fields === 0) return;
|
|
277
|
-
|
|
552
|
+
const value = entry.value ?? entry.seed;
|
|
553
|
+
entry.pending |= value == null || value.stale ? fields | 23 : fields;
|
|
278
554
|
if (baseMoves != null) {
|
|
279
555
|
entry.baseMoves ??= /* @__PURE__ */ new Set();
|
|
280
556
|
for (const ref of baseMoves) entry.baseMoves.add(ref);
|
|
@@ -336,7 +612,8 @@ var WorktreeSummaryService = class {
|
|
|
336
612
|
return true;
|
|
337
613
|
}
|
|
338
614
|
async run(entry, fields, baseMoves) {
|
|
339
|
-
const
|
|
615
|
+
const { value, seed } = entry;
|
|
616
|
+
const previous = value ?? seed;
|
|
340
617
|
let result;
|
|
341
618
|
try {
|
|
342
619
|
const repo = this.getRepository(entry.key);
|
|
@@ -350,16 +627,27 @@ var WorktreeSummaryService = class {
|
|
|
350
627
|
if (entry.disposed) return;
|
|
351
628
|
if (result.summary.stale && (previous == null || !previous.stale)) _logger.warn(`WorktreeSummaryService: reading the base of '${entry.key}' failed: ${String(result.baseError)}`);
|
|
352
629
|
entry.error = void 0;
|
|
630
|
+
entry.seed = void 0;
|
|
353
631
|
entry.baseKeys = result.baseKeys;
|
|
354
632
|
entry.baseConfigKey = entry.pending & baseConfigField ? void 0 : result.baseConfigKey;
|
|
355
633
|
entry.baseDefaultBranch = result.baseDefaultBranch;
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
634
|
+
const dirtyConfirmed = result.dirtyReadAt != null && entry.dirtyReadAt != null;
|
|
635
|
+
if (dirtyConfirmed) clearDeferredDirty(entry);
|
|
636
|
+
if (value != null && summariesEqual(value, result.summary)) {
|
|
637
|
+
if (dirtyConfirmed) this.fireChange(entry, value);
|
|
638
|
+
return;
|
|
639
|
+
}
|
|
640
|
+
const summary = value == null && seed != null && summariesEqual(seed, result.summary) ? seed : result.summary;
|
|
641
|
+
entry.value = summary;
|
|
642
|
+
this.fireChange(entry, summary);
|
|
359
643
|
}
|
|
360
644
|
fail(entry, ex) {
|
|
361
645
|
const previous = entry.value;
|
|
362
646
|
if (previous == null) {
|
|
647
|
+
if (entry.seed != null) {
|
|
648
|
+
dropSeed(entry);
|
|
649
|
+
if (entry.pending !== 0) entry.pending |= 23;
|
|
650
|
+
}
|
|
363
651
|
if (entry.error == null) _logger.warn(`WorktreeSummaryService: reading '${entry.key}' failed: ${String(ex)}`);
|
|
364
652
|
entry.error = ex instanceof Error ? ex : new Error(String(ex));
|
|
365
653
|
return;
|
|
@@ -394,6 +682,8 @@ var WorktreeSummaryService = class {
|
|
|
394
682
|
entry.handle?.dispose();
|
|
395
683
|
entry.handle = void 0;
|
|
396
684
|
entry.value = void 0;
|
|
685
|
+
entry.seed = void 0;
|
|
686
|
+
clearDeferredDirty(entry);
|
|
397
687
|
entry.baseKeys = void 0;
|
|
398
688
|
entry.baseConfigKey = void 0;
|
|
399
689
|
entry.baseDefaultBranch = void 0;
|
|
@@ -412,6 +702,19 @@ function* getConfigRelevantRefs(value) {
|
|
|
412
702
|
if (value.upstream != null) yield `refs/heads/${value.upstream.name}`;
|
|
413
703
|
yield* value.baseDependsOn;
|
|
414
704
|
}
|
|
705
|
+
/** Drops the saved read a first run was to start from, with the values its base read consulted and its dirty flag */
|
|
706
|
+
function dropSeed(entry) {
|
|
707
|
+
entry.seed = void 0;
|
|
708
|
+
clearDeferredDirty(entry);
|
|
709
|
+
entry.baseKeys = void 0;
|
|
710
|
+
entry.baseConfigKey = void 0;
|
|
711
|
+
entry.baseDefaultBranch = void 0;
|
|
712
|
+
}
|
|
713
|
+
function clearDeferredDirty(entry) {
|
|
714
|
+
entry.dirtyReadAt = void 0;
|
|
715
|
+
clearTimeout(entry.dirtyReadTimer);
|
|
716
|
+
entry.dirtyReadTimer = void 0;
|
|
717
|
+
}
|
|
415
718
|
function createEntry(key) {
|
|
416
719
|
return {
|
|
417
720
|
key,
|
|
@@ -421,6 +724,9 @@ function createEntry(key) {
|
|
|
421
724
|
handle: void 0,
|
|
422
725
|
listeners: void 0,
|
|
423
726
|
value: void 0,
|
|
727
|
+
seed: void 0,
|
|
728
|
+
dirtyReadAt: void 0,
|
|
729
|
+
dirtyReadTimer: void 0,
|
|
424
730
|
baseKeys: void 0,
|
|
425
731
|
baseConfigKey: void 0,
|
|
426
732
|
baseDefaultBranch: void 0,
|
|
@@ -443,7 +749,12 @@ function createEntry(key) {
|
|
|
443
749
|
*/
|
|
444
750
|
async function readSummary(repo, key, fields, previous, previousBaseKeys, previousBaseConfigKey, previousBaseDefaultBranch, baseMoves, signal, verifyBaseRemeasure) {
|
|
445
751
|
if (previous == null) fields |= allFields;
|
|
446
|
-
let
|
|
752
|
+
let dirtyReadAt;
|
|
753
|
+
const startDirtyRead = () => {
|
|
754
|
+
dirtyReadAt = Date.now();
|
|
755
|
+
return readDirty(repo, signal);
|
|
756
|
+
};
|
|
757
|
+
let dirtyRead = fields & dirtyField ? startDirtyRead() : void 0;
|
|
447
758
|
let branch = previous?.branch;
|
|
448
759
|
let detached = previous?.detached ?? false;
|
|
449
760
|
let sha = previous?.sha;
|
|
@@ -460,7 +771,7 @@ async function readSummary(repo, key, fields, previous, previousBaseKeys, previo
|
|
|
460
771
|
behind = current?.upstream?.state.behind ?? 0;
|
|
461
772
|
if (previous != null && (branch !== previous.branch || detached !== previous.detached)) {
|
|
462
773
|
fields |= baseField;
|
|
463
|
-
dirtyRead ??=
|
|
774
|
+
dirtyRead ??= startDirtyRead();
|
|
464
775
|
}
|
|
465
776
|
}
|
|
466
777
|
let base = previous?.base;
|
|
@@ -555,7 +866,123 @@ async function readSummary(repo, key, fields, previous, previousBaseKeys, previo
|
|
|
555
866
|
baseKeys,
|
|
556
867
|
baseConfigKey,
|
|
557
868
|
baseDefaultBranch,
|
|
558
|
-
baseError
|
|
869
|
+
baseError,
|
|
870
|
+
dirtyReadAt
|
|
871
|
+
};
|
|
872
|
+
}
|
|
873
|
+
async function getGitDir(repo) {
|
|
874
|
+
try {
|
|
875
|
+
return await repo.config.getGitDir?.();
|
|
876
|
+
} catch (ex) {
|
|
877
|
+
if (isCancellationError(ex)) throw ex;
|
|
878
|
+
return;
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
/** Each ref list's tips by full name, built once per list, which core's cache shares across a repository's worktrees */
|
|
882
|
+
const tipsByNameCache = /* @__PURE__ */ new WeakMap();
|
|
883
|
+
function getTipsByName(tips) {
|
|
884
|
+
let byName = tipsByNameCache.get(tips);
|
|
885
|
+
if (byName == null) {
|
|
886
|
+
const map = /* @__PURE__ */ new Map();
|
|
887
|
+
for (const tip of tips) map.set(tip.fullName, tip.sha);
|
|
888
|
+
byName = map;
|
|
889
|
+
tipsByNameCache.set(tips, byName);
|
|
890
|
+
}
|
|
891
|
+
return byName;
|
|
892
|
+
}
|
|
893
|
+
/**
|
|
894
|
+
* The read groups due on a read from `last`, given what `probe` found of its inputs since (with the refs the base
|
|
895
|
+
* depends on that moved): those `last` made due, and those whose inputs moved
|
|
896
|
+
*/
|
|
897
|
+
async function getMovedFields(repo, last, probe, probeDirty, signal) {
|
|
898
|
+
let fields = last.due;
|
|
899
|
+
let baseMoves;
|
|
900
|
+
const { inputs } = last;
|
|
901
|
+
const previous = last.result.summary;
|
|
902
|
+
if (probe.branch !== inputs.branch || probe.detached !== inputs.detached || probe.sha !== inputs.sha) fields |= allFields;
|
|
903
|
+
else {
|
|
904
|
+
if (probe.upstream !== inputs.upstream) fields |= 18;
|
|
905
|
+
else if (getUpstreamTips(probe.tips, probe.upstream) !== inputs.upstreamTips) fields |= branchField;
|
|
906
|
+
for (const ref of previous.baseDependsOn) {
|
|
907
|
+
const value = getBaseInput(probe, ref);
|
|
908
|
+
if (value !== null && value === inputs.baseTips.get(ref)) continue;
|
|
909
|
+
baseMoves ??= /* @__PURE__ */ new Set();
|
|
910
|
+
baseMoves.add(ref);
|
|
911
|
+
}
|
|
912
|
+
if (baseMoves != null) fields |= baseMoveField;
|
|
913
|
+
if ((fields & baseConfigField) === 0 && last.result.baseConfigKey != null && repo.branches.getAheadOfBase != null && (probe.detached || probe.branch != null)) {
|
|
914
|
+
const configKey = getBaseConfigKey(await readBaseConfig(repo, signal), previous);
|
|
915
|
+
if (configKey != null && configKey !== last.result.baseConfigKey) fields |= 18;
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
if (probeDirty || probe.index == null || probe.index !== inputs.index) fields |= dirtyField;
|
|
919
|
+
return {
|
|
920
|
+
fields,
|
|
921
|
+
baseMoves
|
|
922
|
+
};
|
|
923
|
+
}
|
|
924
|
+
/** The upstream's tips as a remote-tracking and as a local branch, since its short name doesn't say which */
|
|
925
|
+
function getUpstreamTips(tips, upstream) {
|
|
926
|
+
if (upstream == null) return void 0;
|
|
927
|
+
return `${tips.get(`refs/remotes/${upstream}`) ?? ""}\0${tips.get(`refs/heads/${upstream}`) ?? ""}`;
|
|
928
|
+
}
|
|
929
|
+
/**
|
|
930
|
+
* Reads where the `HEAD` symref of each of `remotes` (every remote's when `undefined`) points (`origin/main`) into
|
|
931
|
+
* `heads`, as core's cached local reads answer it, skipping those already there: `undefined` when it has none (a
|
|
932
|
+
* remote since removed included), `null` when the read failed
|
|
933
|
+
*/
|
|
934
|
+
async function readRemoteHeads(repo, heads, remotes, signal) {
|
|
935
|
+
if (remotes?.size === 0) return;
|
|
936
|
+
const names = remotes ?? (await repo.remotes.getRemotes(void 0, signal)).map((r) => r.name);
|
|
937
|
+
await Promise.all(Array.from(names, async (name) => {
|
|
938
|
+
if (heads.has(name)) return;
|
|
939
|
+
let target;
|
|
940
|
+
try {
|
|
941
|
+
target = await repo.branches.getDefaultBranchName(name, { local: true }, signal);
|
|
942
|
+
} catch (ex) {
|
|
943
|
+
if (isCancellationError(ex)) throw ex;
|
|
944
|
+
target = null;
|
|
945
|
+
}
|
|
946
|
+
heads.set(name, target);
|
|
947
|
+
}));
|
|
948
|
+
}
|
|
949
|
+
/** The remote whose `HEAD` symref `ref` is; `undefined` for any other ref */
|
|
950
|
+
function getHeadRemote(ref) {
|
|
951
|
+
if (!ref.startsWith("refs/remotes/") || !ref.endsWith("/HEAD")) return void 0;
|
|
952
|
+
return ref.slice(13, -5);
|
|
953
|
+
}
|
|
954
|
+
/** The remotes whose `HEAD` symref is among `refs` */
|
|
955
|
+
function getHeadRemotes(refs) {
|
|
956
|
+
const remotes = /* @__PURE__ */ new Set();
|
|
957
|
+
for (const ref of refs) {
|
|
958
|
+
const remote = getHeadRemote(ref);
|
|
959
|
+
if (remote != null) remotes.add(remote);
|
|
960
|
+
}
|
|
961
|
+
return remotes;
|
|
962
|
+
}
|
|
963
|
+
/**
|
|
964
|
+
* What `probe` found of `ref`, a ref the base depends on: for a remote's `HEAD` symref, which the ref tips leave out,
|
|
965
|
+
* the ref it points at, since its tip follows that ref's, itself a dependency; else its tip. `null` for a symref of a
|
|
966
|
+
* remote it didn't read: only when the read found HEAD elsewhere than the probe did and read the base for that, which
|
|
967
|
+
* the next probe sees as HEAD moving anyway
|
|
968
|
+
*/
|
|
969
|
+
function getBaseInput(probe, ref) {
|
|
970
|
+
const remote = getHeadRemote(ref);
|
|
971
|
+
if (remote == null) return probe.tips.get(ref);
|
|
972
|
+
return probe.remoteHeads.has(remote) ? probe.remoteHeads.get(remote) : null;
|
|
973
|
+
}
|
|
974
|
+
/** The inputs `summary` was read from, as the probe before its read found them */
|
|
975
|
+
function toUnwatchedInputs(probe, summary) {
|
|
976
|
+
const baseTips = /* @__PURE__ */ new Map();
|
|
977
|
+
for (const ref of summary.baseDependsOn) baseTips.set(ref, getBaseInput(probe, ref));
|
|
978
|
+
return {
|
|
979
|
+
branch: probe.branch,
|
|
980
|
+
detached: probe.detached,
|
|
981
|
+
sha: probe.sha,
|
|
982
|
+
upstream: probe.upstream,
|
|
983
|
+
upstreamTips: getUpstreamTips(probe.tips, probe.upstream),
|
|
984
|
+
baseTips,
|
|
985
|
+
index: probe.index
|
|
559
986
|
};
|
|
560
987
|
}
|
|
561
988
|
async function readBaseConfig(repo, signal) {
|