@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
|
@@ -1,15 +1,28 @@
|
|
|
1
1
|
import { some } from "../../utils/iterable.js";
|
|
2
|
-
import { isDescendant, joinPaths, relative } from "../../utils/path.js";
|
|
2
|
+
import { dirname, isAbsolute, isDescendant, joinPaths, normalizePath, relative } from "../../utils/path.js";
|
|
3
|
+
import { FileType } from "../context.js";
|
|
3
4
|
import { GitDirProbeTarget } from "./gitDirPoller.js";
|
|
4
|
-
import { dotGitGlobCombined, dotGitGlobCommon, dotGitGlobRoot, getGitDirRefProbes, gitDirCommonProbes, gitDirExcludes, gitDirProbesByEmittedPath, gitDirRootProbes, shouldIgnoreRepoPath } from "./watcherPatterns.js";
|
|
5
|
+
import { dotGitGlobCombined, dotGitGlobCommon, dotGitGlobRoot, getGitDirRefProbes, getIgnoredWorktreeFolderExcludes, gitDirCommonProbes, gitDirExcludes, gitDirProbesByEmittedPath, gitDirRootProbes, shouldIgnoreRepoPath } from "./watcherPatterns.js";
|
|
5
6
|
import { allWatchedRepositoryChanges, classifyGitDirChange } from "./classifyChange.js";
|
|
6
7
|
import { diffGitConfigSections, parseGitConfigBytes } from "./configSections.js";
|
|
7
8
|
//#region ../git/src/watching/watchGroup.ts
|
|
8
9
|
/**
|
|
9
|
-
* How long a read of the shared `.git/config
|
|
10
|
-
* time, so one that never settles, as on a hung network or WSL mount, would otherwise hold
|
|
10
|
+
* How long a read of the shared `.git/config`, or of the worktrees' admin entries, may take before it counts as failed.
|
|
11
|
+
* Reads of each run one at a time, so one that never settles, as on a hung network or WSL mount, would otherwise hold
|
|
12
|
+
* back every later report.
|
|
11
13
|
*/
|
|
12
|
-
const
|
|
14
|
+
const readTimeoutMs = 5e3;
|
|
15
|
+
/**
|
|
16
|
+
* How many removed admin entries are remembered. Each watcher reports a removal once for each file it watched in the
|
|
17
|
+
* entry, and two watchers see the common dir beside a main checkout, so its reports outlive any single one of them
|
|
18
|
+
*/
|
|
19
|
+
const removedWorktreeIdsLimit = 64;
|
|
20
|
+
/**
|
|
21
|
+
* How long after an admin entry's `gitdir` couldn't be read it is read once more. A watcher can report the entry before
|
|
22
|
+
* git wrote its `gitdir`, and then miss the write itself (a new directory's files go unseen until it is watched)
|
|
23
|
+
*/
|
|
24
|
+
const worktreeRetryDelayMs = 250;
|
|
25
|
+
function noop() {}
|
|
13
26
|
/**
|
|
14
27
|
* Manages filesystem watchers for a single physical `.git` directory,
|
|
15
28
|
* shared across all worktrees that reference it.
|
|
@@ -30,12 +43,42 @@ const configReadTimeoutMs = 5e3;
|
|
|
30
43
|
* starts, is the baseline and delivers nothing; a report arriving before it completes may describe a change it
|
|
31
44
|
* already holds, so it is delivered unconfined. A read that fails or doesn't settle within a few seconds, or a file
|
|
32
45
|
* git couldn't parse, is unconfined too.
|
|
46
|
+
*
|
|
47
|
+
* Given a `readFile`, a `readDirectory` (a directory's entries with their types, only the directories counting as
|
|
48
|
+
* admin entries) and the folders whose worktrees the host has core ignore (a tool's throwaway worktrees, which no
|
|
49
|
+
* host shows), the group reports no change of an ignored worktree's admin entry
|
|
50
|
+
* (`<common>/worktrees/<id>`) to any session. Only registered folders count, since a user may keep real worktrees
|
|
51
|
+
* anywhere, `.git` included. It lists the entries and reads each one's `gitdir` once as it starts, and again after a
|
|
52
|
+
* watcher dropped events; a report arriving meanwhile is told at once, and decided once the listing settles, so what
|
|
53
|
+
* is known of its entry catches up. From then on it reads an entry only when its `gitdir` may be new (the entry was
|
|
54
|
+
* created, or its `gitdir` written, or it was removed before), so a real worktree's HEAD moves are told at once,
|
|
55
|
+
* unread. Of two reads of one entry, the later decides both reports. An entry it doesn't know whose `gitdir` can't be
|
|
56
|
+
* read (a watcher can report one before git writes it, and miss the write) is read once more a moment later, its
|
|
57
|
+
* reports held until then, and told if it still can't be. Only the entry's directory or its `gitdir` going is a
|
|
58
|
+
* removal; a deleted `HEAD` (a watcher may report git's lock file renamed over it as a delete) or `locked` is told as
|
|
59
|
+
* the entry is known. Removing an entry deletes the `gitdir` a read needs, so a removal is told from what the entry was
|
|
60
|
+
* known to be (before a read that couldn't read it, if one is waiting to read it again), and an entry it never knew is
|
|
61
|
+
* told as real. Any other report of a removed entry but a delete is read, and one that can't be read is the removal's
|
|
62
|
+
* own (a watcher may report the entry's directory changed beside each file it deletes), unless it creates the entry
|
|
63
|
+
* again. A report of the `worktrees` directory itself (a coalesced
|
|
64
|
+
* removal, a parent directory change, a poll, which sees nothing finer) lists it again: it is told to every session
|
|
65
|
+
* when a real entry was added or removed since the last listing, and to none when only ignored ones were. Git removes
|
|
66
|
+
* the directory with its last entry, so a directory that isn't there is an empty listing, as is a removal of it that
|
|
67
|
+
* can't be listed. Since a listing (or a worktree moved into or out of a folder) changes what every session shows, it
|
|
68
|
+
* is told to all of them; the other watcher's report of the same change then finds nothing left to tell. Where only a
|
|
69
|
+
* poll watches the common dir, it sees just the `worktrees` directory, so an entry removed and added again under the
|
|
70
|
+
* same id between two polls, or an entry's HEAD or lock changed in place, goes untold.
|
|
71
|
+
*
|
|
72
|
+
* Given the folders, the watchers over the common dir leave each one unwatched, since a tool's checkout there would
|
|
73
|
+
* otherwise have every directory of it watched; one that shares a top-level directory with what the `.git` watch reads
|
|
74
|
+
* stays watched.
|
|
33
75
|
*/
|
|
34
76
|
var WatchGroup = class {
|
|
35
77
|
commonGitDir;
|
|
36
78
|
provider;
|
|
37
79
|
poller;
|
|
38
80
|
readFile;
|
|
81
|
+
readDirectory;
|
|
39
82
|
_sessions = /* @__PURE__ */ new Map();
|
|
40
83
|
_commonWatcher;
|
|
41
84
|
_commonReliability = "reliable";
|
|
@@ -47,12 +90,50 @@ var WatchGroup = class {
|
|
|
47
90
|
_configBaselining = false;
|
|
48
91
|
_configReading = false;
|
|
49
92
|
_configRereadQueued = false;
|
|
50
|
-
|
|
93
|
+
/**
|
|
94
|
+
* Settles once the latest baseline of the admin entries was taken; `undefined` when the group can't tell an
|
|
95
|
+
* ignored worktree's, so every report is told at once
|
|
96
|
+
*/
|
|
97
|
+
_worktreesBaseline;
|
|
98
|
+
/** A baseline is pending, so a report waits for it */
|
|
99
|
+
_worktreesBaselining = false;
|
|
100
|
+
_worktreesRebaselineQueued = false;
|
|
101
|
+
/** Set by the first baseline; while empty, nothing is ignored and every report is told at once */
|
|
102
|
+
_ignoredWorktreeRoots = [];
|
|
103
|
+
/** Listings run one at a time, so each compares with the one before it */
|
|
104
|
+
_worktreesQueue = Promise.resolve();
|
|
105
|
+
/** The `worktrees` directory's reports waiting on the queued listing; `undefined` while none is queued */
|
|
106
|
+
_worktreesRescanQueued;
|
|
107
|
+
/** The admin entries the last listing found */
|
|
108
|
+
_listedWorktreeIds = /* @__PURE__ */ new Set();
|
|
109
|
+
/** Admin entries known to exist, by id: whether each is an ignored worktree's */
|
|
110
|
+
_worktreeIds = /* @__PURE__ */ new Map();
|
|
111
|
+
/** What removed admin entries were, by id, oldest first */
|
|
112
|
+
_removedWorktreeIds = /* @__PURE__ */ new Map();
|
|
113
|
+
/**
|
|
114
|
+
* Each id's latest `gitdir` read, settling with what it found and what the id was known to be before it; only it
|
|
115
|
+
* may update what is known of the id. A removal of the entry drops it
|
|
116
|
+
*/
|
|
117
|
+
_worktreeReads = /* @__PURE__ */ new Map();
|
|
118
|
+
/** Admin entries whose `gitdir` couldn't be read, by id, waiting to be read once more */
|
|
119
|
+
_worktreeRetries = /* @__PURE__ */ new Map();
|
|
120
|
+
/** The `.git` excludes of a watcher over the common git dir, with the ignored worktrees' folders it may leave out */
|
|
121
|
+
_commonGitDirExcludes;
|
|
122
|
+
constructor(commonGitDir, provider, poller, readFile, readDirectory, ignoredWorktreeRoots, ignoredWorktreeFolders) {
|
|
51
123
|
this.commonGitDir = commonGitDir;
|
|
52
124
|
this.provider = provider;
|
|
53
125
|
this.poller = poller;
|
|
54
126
|
this.readFile = readFile;
|
|
127
|
+
this.readDirectory = readDirectory;
|
|
128
|
+
const folders = getIgnoredWorktreeFolderExcludes(ignoredWorktreeFolders);
|
|
129
|
+
this._commonGitDirExcludes = folders.length ? [...gitDirExcludes, ...folders] : gitDirExcludes;
|
|
55
130
|
if (readFile != null) this.readConfigBaseline();
|
|
131
|
+
if (readFile != null && readDirectory != null && ignoredWorktreeRoots != null) this.queueWorktreesBaseline(async () => {
|
|
132
|
+
const roots = await this.withTimeout(() => Promise.resolve(ignoredWorktreeRoots));
|
|
133
|
+
if (this._disposed || !Array.isArray(roots) || !roots.length) return;
|
|
134
|
+
this._ignoredWorktreeRoots = roots;
|
|
135
|
+
await this.listWorktrees("baseline");
|
|
136
|
+
});
|
|
56
137
|
}
|
|
57
138
|
[Symbol.dispose]() {
|
|
58
139
|
this.dispose();
|
|
@@ -67,6 +148,11 @@ var WatchGroup = class {
|
|
|
67
148
|
}
|
|
68
149
|
this._sessions.clear();
|
|
69
150
|
this.disposeCommonWatcher();
|
|
151
|
+
for (const retry of this._worktreeRetries.values()) {
|
|
152
|
+
clearTimeout(retry.timer);
|
|
153
|
+
retry.resolve(void 0);
|
|
154
|
+
}
|
|
155
|
+
this._worktreeRetries.clear();
|
|
70
156
|
}
|
|
71
157
|
get sessions() {
|
|
72
158
|
return this._sessions;
|
|
@@ -91,7 +177,7 @@ var WatchGroup = class {
|
|
|
91
177
|
if (event.reason !== "overflow") e.target?.forget(event.path);
|
|
92
178
|
this.onRootEvent(event, e);
|
|
93
179
|
}
|
|
94
|
-
}, this.getWatcherOptions((state) => {
|
|
180
|
+
}, this.getWatcherOptions(gitDir.uri.fsPath, (state) => {
|
|
95
181
|
if (entry == null) initialReliability = state;
|
|
96
182
|
else if (this._sessions.get(repoPath) === entry) this.setRootReliability(entry, state);
|
|
97
183
|
}));
|
|
@@ -129,7 +215,7 @@ var WatchGroup = class {
|
|
|
129
215
|
if (this._commonWatcher != null || this._disposed) return;
|
|
130
216
|
let watcher;
|
|
131
217
|
let initialReliability = "reliable";
|
|
132
|
-
const options = this.getWatcherOptions((state) => {
|
|
218
|
+
const options = this.getWatcherOptions(this.commonGitDir, (state) => {
|
|
133
219
|
if (watcher == null) initialReliability = state;
|
|
134
220
|
else if (this._commonWatcher === watcher) this.setCommonReliability(state);
|
|
135
221
|
});
|
|
@@ -150,11 +236,21 @@ var WatchGroup = class {
|
|
|
150
236
|
}
|
|
151
237
|
}
|
|
152
238
|
/** Hosts that didn't opt into polling get exactly the options they always had */
|
|
153
|
-
getWatcherOptions(onDidChangeReliability) {
|
|
239
|
+
getWatcherOptions(basePath, onDidChangeReliability) {
|
|
240
|
+
const excludes = this.getExcludes(basePath);
|
|
154
241
|
return this.poller != null ? {
|
|
155
|
-
excludes
|
|
242
|
+
excludes,
|
|
156
243
|
onDidChangeReliability
|
|
157
|
-
} : { excludes
|
|
244
|
+
} : { excludes };
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* The ignored worktrees' folders join the `.git` excludes of a watcher over the common git dir: a tool that keeps its
|
|
248
|
+
* throwaway checkouts inside the git dir would otherwise have each one's every directory watched, and every file it
|
|
249
|
+
* writes reported. Every other watcher sits on a linked worktree's admin entry, which holds none of them
|
|
250
|
+
*/
|
|
251
|
+
getExcludes(basePath) {
|
|
252
|
+
if (this._commonGitDirExcludes === gitDirExcludes) return gitDirExcludes;
|
|
253
|
+
return normalizePath(basePath) === normalizePath(this.commonGitDir) ? this._commonGitDirExcludes : gitDirExcludes;
|
|
158
254
|
}
|
|
159
255
|
setRootReliability(entry, state, initial) {
|
|
160
256
|
const previous = entry.reliability;
|
|
@@ -171,7 +267,10 @@ var WatchGroup = class {
|
|
|
171
267
|
this.poller.add(target);
|
|
172
268
|
}
|
|
173
269
|
if (state === "unwatchable" === (previous === "unwatchable")) return;
|
|
174
|
-
if (!initial)
|
|
270
|
+
if (!initial) {
|
|
271
|
+
this.deliverOverflow(entry);
|
|
272
|
+
if (entry.coversCommon) this.rebaselineWorktrees();
|
|
273
|
+
}
|
|
175
274
|
entry.hooks.onUnwatchableChanged?.(entry.repoPath);
|
|
176
275
|
}
|
|
177
276
|
setCommonReliability(state, initial) {
|
|
@@ -196,6 +295,7 @@ var WatchGroup = class {
|
|
|
196
295
|
if (!initial) this.deliverOverflow(entry);
|
|
197
296
|
entry.hooks.onUnwatchableChanged?.(entry.repoPath);
|
|
198
297
|
}
|
|
298
|
+
if (!initial) this.rebaselineWorktrees();
|
|
199
299
|
}
|
|
200
300
|
stopPolling(entry) {
|
|
201
301
|
if (entry.target == null) return;
|
|
@@ -242,6 +342,7 @@ var WatchGroup = class {
|
|
|
242
342
|
if (entry.coversCommon) continue;
|
|
243
343
|
this.deliverOverflow(entry);
|
|
244
344
|
}
|
|
345
|
+
this.rebaselineWorktrees();
|
|
245
346
|
return;
|
|
246
347
|
}
|
|
247
348
|
if (!isDescendant(event.path, this.commonGitDir)) return;
|
|
@@ -253,7 +354,7 @@ var WatchGroup = class {
|
|
|
253
354
|
}
|
|
254
355
|
const isFetchHead = relativePath === "FETCH_HEAD";
|
|
255
356
|
const isExclude = relativePath === "info/exclude";
|
|
256
|
-
const changes = isFetchHead ? void 0 : classifyGitDirChange(relativePath);
|
|
357
|
+
const changes = isFetchHead ? void 0 : withWorktreeEntryHead(classifyGitDirChange(relativePath), relativePath, event.reason);
|
|
257
358
|
if (changes == null && !isFetchHead && !isExclude) return;
|
|
258
359
|
const ref = changes != null ? getChangedRef(relativePath) : void 0;
|
|
259
360
|
let ownerId;
|
|
@@ -261,6 +362,16 @@ var WatchGroup = class {
|
|
|
261
362
|
const idEnd = relativePath.indexOf("/", 10);
|
|
262
363
|
if (idEnd !== -1 && relativePath.substring(idEnd + 1) === "HEAD") ownerId = relativePath.substring(10, idEnd);
|
|
263
364
|
}
|
|
365
|
+
if (changes != null && this.isWorktreesReport(changes)) {
|
|
366
|
+
const sessions = [...this._sessions.values()].filter((e) => !e.coversCommon && (ownerId == null || e.worktreeId !== ownerId));
|
|
367
|
+
this.onWorktreesReported(relativePath, event.reason, () => {
|
|
368
|
+
for (const entry of sessions) {
|
|
369
|
+
if (this._disposed || this._sessions.get(entry.repoPath) !== entry) continue;
|
|
370
|
+
entry.hooks.onRepoChanged(entry.repoPath, changes, ref);
|
|
371
|
+
}
|
|
372
|
+
});
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
264
375
|
for (const entry of this._sessions.values()) {
|
|
265
376
|
if (entry.coversCommon) continue;
|
|
266
377
|
if (isFetchHead) {
|
|
@@ -279,6 +390,7 @@ var WatchGroup = class {
|
|
|
279
390
|
onRootEvent(event, entry) {
|
|
280
391
|
if (event.reason === "overflow") {
|
|
281
392
|
this.deliverOverflow(entry);
|
|
393
|
+
if (entry.coversCommon) this.rebaselineWorktrees();
|
|
282
394
|
return;
|
|
283
395
|
}
|
|
284
396
|
if (!isDescendant(event.path, entry.gitDirPath)) return;
|
|
@@ -292,10 +404,273 @@ var WatchGroup = class {
|
|
|
292
404
|
this.onConfigReported();
|
|
293
405
|
return;
|
|
294
406
|
}
|
|
295
|
-
const changes = classifyGitDirChange(relativePath);
|
|
296
|
-
if (changes != null
|
|
407
|
+
const changes = withWorktreeEntryHead(classifyGitDirChange(relativePath), relativePath, event.reason);
|
|
408
|
+
if (changes != null && entry.coversCommon && this.isWorktreesReport(changes)) this.onWorktreesReported(relativePath, event.reason, () => {
|
|
409
|
+
if (this._disposed || this._sessions.get(entry.repoPath) !== entry) return;
|
|
410
|
+
entry.hooks.onRepoChanged(entry.repoPath, changes, getChangedRef(relativePath));
|
|
411
|
+
});
|
|
412
|
+
else if (changes != null) entry.hooks.onRepoChanged(entry.repoPath, changes, getChangedRef(relativePath));
|
|
297
413
|
if (relativePath === "info/exclude") entry.hooks.onIgnoresChanged?.(entry.repoPath);
|
|
298
414
|
}
|
|
415
|
+
/** A report of the `worktrees` directory, an admin entry or a file in it, once the group may tell them apart */
|
|
416
|
+
isWorktreesReport(changes) {
|
|
417
|
+
return this._worktreesBaseline != null && changes.includes("worktrees") && changes.every((c) => c === "worktrees" || c === "worktreeHead");
|
|
418
|
+
}
|
|
419
|
+
/** `deliver` tells the sessions the report reaches of it, should the report concern them (see above) */
|
|
420
|
+
onWorktreesReported(relativePath, reason, deliver) {
|
|
421
|
+
if (this._worktreesBaselining && this._worktreesBaseline != null) {
|
|
422
|
+
deliver();
|
|
423
|
+
this._worktreesBaseline.then(() => this.decideWorktreesReport(relativePath, reason, noop));
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
this.decideWorktreesReport(relativePath, reason, deliver);
|
|
427
|
+
}
|
|
428
|
+
decideWorktreesReport(relativePath, reason, deliver) {
|
|
429
|
+
if (this._disposed) return;
|
|
430
|
+
if (!this._ignoredWorktreeRoots.length) {
|
|
431
|
+
deliver();
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
if (relativePath === "worktrees") {
|
|
435
|
+
this.queueWorktreesRescan(reason, deliver);
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
const idEnd = relativePath.indexOf("/", 10);
|
|
439
|
+
const id = idEnd === -1 ? relativePath.substring(10) : relativePath.substring(10, idEnd);
|
|
440
|
+
const file = idEnd === -1 ? void 0 : relativePath.substring(idEnd + 1);
|
|
441
|
+
if (reason === "delete" && (file == null || file === "gitdir")) {
|
|
442
|
+
if (this.forgetWorktreeId(id) !== true) deliver();
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
const removed = this._worktreeIds.has(id) ? void 0 : this._removedWorktreeIds.get(id);
|
|
446
|
+
const known = this._worktreeIds.get(id) ?? (reason === "delete" ? removed : void 0);
|
|
447
|
+
if (known != null && file !== "gitdir" && !(file == null && reason === "create")) {
|
|
448
|
+
if (!known) deliver();
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
451
|
+
const retry = removed == null || reason === "create";
|
|
452
|
+
this.readWorktreeId(id, retry).then(([ignored, previous]) => {
|
|
453
|
+
if (this._disposed) return;
|
|
454
|
+
if (ignored == null) {
|
|
455
|
+
if (retry) this.waitForWorktreeRetry(id, deliver);
|
|
456
|
+
else if (removed === false) deliver();
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
if (previous != null && ignored !== previous) {
|
|
460
|
+
this.deliverWorktreesChange();
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
if (!ignored) deliver();
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
/**
|
|
467
|
+
* Reads the entry's `gitdir`: whether it names an ignored worktree (`undefined` when it can't be read), and what the
|
|
468
|
+
* id was known to be when this read replaced it. Reads of one id can settle out of order (a slow one from before the
|
|
469
|
+
* worktree moved out of the folder), so only the latest updates what is known of it; an earlier one settles with
|
|
470
|
+
* what the latest finds, without what the id was, since the latest tells a move itself, or once the entry was removed
|
|
471
|
+
* since, with what the removal found it was. One that can't read the entry forgets what it was known to be, since git
|
|
472
|
+
* may be writing its `gitdir` again, and reads it once more a moment later, unless `retry` is false (it then leaves
|
|
473
|
+
* what is known as it was); one that can settles that read, if pending.
|
|
474
|
+
*/
|
|
475
|
+
readWorktreeId(id, retry = true) {
|
|
476
|
+
const read = this.readIgnoredWorktree(id).then((ignored) => {
|
|
477
|
+
const latest = this._worktreeReads.get(id);
|
|
478
|
+
if (latest !== read) {
|
|
479
|
+
if (latest == null) return [this._removedWorktreeIds.get(id), void 0];
|
|
480
|
+
return latest.then(([found]) => {
|
|
481
|
+
if (found == null && retry && this._worktreeReads.has(id)) this.scheduleWorktreeRetry(id, void 0);
|
|
482
|
+
return [found, void 0];
|
|
483
|
+
});
|
|
484
|
+
}
|
|
485
|
+
const previous = this._worktreeIds.get(id);
|
|
486
|
+
if (ignored == null) {
|
|
487
|
+
if (!retry) return [void 0, previous];
|
|
488
|
+
this._removedWorktreeIds.delete(id);
|
|
489
|
+
this._worktreeIds.delete(id);
|
|
490
|
+
this.scheduleWorktreeRetry(id, previous);
|
|
491
|
+
return [void 0, previous];
|
|
492
|
+
}
|
|
493
|
+
this._removedWorktreeIds.delete(id);
|
|
494
|
+
this._worktreeIds.set(id, ignored);
|
|
495
|
+
this.settleWorktreeRetry(id, ignored);
|
|
496
|
+
return [ignored, previous];
|
|
497
|
+
});
|
|
498
|
+
this._worktreeReads.set(id, read);
|
|
499
|
+
return read;
|
|
500
|
+
}
|
|
501
|
+
/** Reads the entry once more a moment later, unless that is already pending; `previous` is what it was known to be */
|
|
502
|
+
scheduleWorktreeRetry(id, previous) {
|
|
503
|
+
if (this._disposed) return;
|
|
504
|
+
const pending = this._worktreeRetries.get(id);
|
|
505
|
+
if (pending != null) {
|
|
506
|
+
pending.previous ??= previous;
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
509
|
+
let resolve;
|
|
510
|
+
const decided = new Promise((r) => {
|
|
511
|
+
resolve = r;
|
|
512
|
+
});
|
|
513
|
+
const timer = setTimeout(() => {
|
|
514
|
+
this.readWorktreeId(id).then(([ignored]) => {
|
|
515
|
+
if (this._worktreeRetries.get(id)?.decided === decided) this.settleWorktreeRetry(id, ignored);
|
|
516
|
+
});
|
|
517
|
+
}, worktreeRetryDelayMs);
|
|
518
|
+
this._worktreeRetries.set(id, {
|
|
519
|
+
timer,
|
|
520
|
+
delivers: [],
|
|
521
|
+
previous,
|
|
522
|
+
decided,
|
|
523
|
+
resolve
|
|
524
|
+
});
|
|
525
|
+
}
|
|
526
|
+
/**
|
|
527
|
+
* A report whose read couldn't read the entry waits on the entry's next read, and `deliver` is told then, unless that
|
|
528
|
+
* read finds an ignored worktree's; one still unreadable is told. Both watchers report each change of an entry, so
|
|
529
|
+
* the reports of one id wait on one read. Resolves with what it found, as {@link settleWorktreeRetry} does
|
|
530
|
+
*/
|
|
531
|
+
waitForWorktreeRetry(id, deliver) {
|
|
532
|
+
const known = this._worktreeIds.get(id);
|
|
533
|
+
if (known != null) {
|
|
534
|
+
if (!known) deliver?.();
|
|
535
|
+
return Promise.resolve(known);
|
|
536
|
+
}
|
|
537
|
+
const retry = this._worktreeRetries.get(id);
|
|
538
|
+
if (retry == null) return Promise.resolve(void 0);
|
|
539
|
+
if (deliver != null) retry.delivers.push(deliver);
|
|
540
|
+
return retry.decided;
|
|
541
|
+
}
|
|
542
|
+
/**
|
|
543
|
+
* Tells the reports waiting on the id's retry what the entry turned out to be: nothing when an ignored worktree's,
|
|
544
|
+
* every session when it moved into or out of an ignored folder, and the reports' own sessions otherwise, `ignored`
|
|
545
|
+
* being `undefined` when it still couldn't be read
|
|
546
|
+
*/
|
|
547
|
+
settleWorktreeRetry(id, ignored) {
|
|
548
|
+
const pending = this._worktreeRetries.get(id);
|
|
549
|
+
if (pending == null) return;
|
|
550
|
+
this._worktreeRetries.delete(id);
|
|
551
|
+
clearTimeout(pending.timer);
|
|
552
|
+
pending.resolve(ignored);
|
|
553
|
+
if (ignored != null && pending.previous != null && ignored !== pending.previous) this.deliverWorktreesChange();
|
|
554
|
+
else if (ignored !== true) for (const deliver of pending.delivers) deliver();
|
|
555
|
+
}
|
|
556
|
+
/**
|
|
557
|
+
* Takes `id` as removed, remembering what it was for the removal's other reports; returns that, or `undefined` when
|
|
558
|
+
* the id was never known
|
|
559
|
+
*/
|
|
560
|
+
forgetWorktreeId(id) {
|
|
561
|
+
this._worktreeReads.delete(id);
|
|
562
|
+
let ignored = this._worktreeIds.get(id);
|
|
563
|
+
const retry = this._worktreeRetries.get(id);
|
|
564
|
+
if (retry != null) {
|
|
565
|
+
this._worktreeRetries.delete(id);
|
|
566
|
+
clearTimeout(retry.timer);
|
|
567
|
+
retry.resolve(void 0);
|
|
568
|
+
ignored ??= retry.previous;
|
|
569
|
+
}
|
|
570
|
+
if (ignored == null) return this._removedWorktreeIds.get(id);
|
|
571
|
+
this._worktreeIds.delete(id);
|
|
572
|
+
this._removedWorktreeIds.delete(id);
|
|
573
|
+
this._removedWorktreeIds.set(id, ignored);
|
|
574
|
+
if (this._removedWorktreeIds.size > removedWorktreeIdsLimit) {
|
|
575
|
+
const [oldest] = this._removedWorktreeIds.keys();
|
|
576
|
+
this._removedWorktreeIds.delete(oldest);
|
|
577
|
+
}
|
|
578
|
+
return ignored;
|
|
579
|
+
}
|
|
580
|
+
/** Whether the entry's `gitdir` names an ignored worktree, or `undefined` when it can't be read. Never rejects */
|
|
581
|
+
async readIgnoredWorktree(id) {
|
|
582
|
+
const adminDir = joinPaths(this.commonGitDir, "worktrees", id);
|
|
583
|
+
const content = await this.readWithTimeout(joinPaths(adminDir, "gitdir"));
|
|
584
|
+
const dotGit = content != null ? new TextDecoder().decode(content).trim() : "";
|
|
585
|
+
const dotGitPath = dotGit ? normalizePath(isAbsolute(dotGit) ? dotGit : joinPaths(adminDir, dotGit)) : "";
|
|
586
|
+
if (!dotGitPath.endsWith("/.git")) return void 0;
|
|
587
|
+
const worktreePath = dirname(dotGitPath);
|
|
588
|
+
return this._ignoredWorktreeRoots.some((root) => isDescendant(worktreePath, root));
|
|
589
|
+
}
|
|
590
|
+
queueWorktreesBaseline(take) {
|
|
591
|
+
this._worktreesBaselining = true;
|
|
592
|
+
const baseline = this._worktreesQueue.then(take).catch(() => {}).finally(() => {
|
|
593
|
+
if (this._worktreesBaseline === baseline) this._worktreesBaselining = false;
|
|
594
|
+
});
|
|
595
|
+
this._worktreesBaseline = baseline;
|
|
596
|
+
this._worktreesQueue = baseline;
|
|
597
|
+
}
|
|
598
|
+
/** A watcher of the `worktrees` directory dropped events: whatever changed meanwhile went unseen */
|
|
599
|
+
rebaselineWorktrees() {
|
|
600
|
+
if (this._worktreesBaseline == null || this._worktreesRebaselineQueued || this._disposed) return;
|
|
601
|
+
if (!this._worktreesBaselining && !this._ignoredWorktreeRoots.length) return;
|
|
602
|
+
this._worktreesRebaselineQueued = true;
|
|
603
|
+
this.queueWorktreesBaseline(async () => {
|
|
604
|
+
this._worktreesRebaselineQueued = false;
|
|
605
|
+
if (this._disposed || !this._ignoredWorktreeRoots.length) return;
|
|
606
|
+
await this.listWorktrees("rebaseline");
|
|
607
|
+
});
|
|
608
|
+
}
|
|
609
|
+
/** The watchers' reports of the directory pile onto one queued listing, which tells them apart */
|
|
610
|
+
queueWorktreesRescan(reason, deliver) {
|
|
611
|
+
const queued = this._worktreesRescanQueued;
|
|
612
|
+
if (queued != null) {
|
|
613
|
+
queued.delivers.push(deliver);
|
|
614
|
+
queued.removal ||= reason === "delete";
|
|
615
|
+
return;
|
|
616
|
+
}
|
|
617
|
+
const rescan = {
|
|
618
|
+
delivers: [deliver],
|
|
619
|
+
removal: reason === "delete"
|
|
620
|
+
};
|
|
621
|
+
this._worktreesRescanQueued = rescan;
|
|
622
|
+
this._worktreesQueue = this._worktreesQueue.then(() => {
|
|
623
|
+
this._worktreesRescanQueued = void 0;
|
|
624
|
+
return this.listWorktrees("rescan", rescan.delivers, rescan.removal);
|
|
625
|
+
}).catch(() => {});
|
|
626
|
+
}
|
|
627
|
+
/**
|
|
628
|
+
* Lists the admin entries and compares them with the last listing. A baseline (or a rebaseline, after a watcher
|
|
629
|
+
* dropped events) reads every entry; a rescan only those it doesn't know, reading one it can't read once more a
|
|
630
|
+
* moment later. Git removes the directory with its last entry, so a directory that isn't there is an empty listing,
|
|
631
|
+
* whatever the reports said of it (a poll sees its removal as a change). A listing that fails otherwise is a
|
|
632
|
+
* baseline's empty directory, and a rescan's too when one of its reports (`delivers`) removed the directory. For any
|
|
633
|
+
* other rescan, it is a change it can't rule out, told to the reports' own sessions
|
|
634
|
+
*/
|
|
635
|
+
async listWorktrees(mode, delivers, removal) {
|
|
636
|
+
const readDirectory = this.readDirectory;
|
|
637
|
+
if (readDirectory == null) return;
|
|
638
|
+
const entries = await this.withTimeout(() => readDirectory(joinPaths(this.commonGitDir, "worktrees")).catch((ex) => {
|
|
639
|
+
if (isNotFound(ex)) return [];
|
|
640
|
+
throw ex;
|
|
641
|
+
}));
|
|
642
|
+
if (this._disposed) return;
|
|
643
|
+
const names = entries?.filter(([, type]) => type & FileType.Directory).map(([name]) => name);
|
|
644
|
+
if (names == null && mode === "rescan" && !removal) {
|
|
645
|
+
for (const deliver of delivers ?? []) deliver();
|
|
646
|
+
return;
|
|
647
|
+
}
|
|
648
|
+
const listing = new Set(names);
|
|
649
|
+
const previous = this._listedWorktreeIds;
|
|
650
|
+
this._listedWorktreeIds = listing;
|
|
651
|
+
let changed = false;
|
|
652
|
+
if (mode !== "baseline") for (const id of /* @__PURE__ */ new Set([...previous, ...this._worktreeIds.keys()])) {
|
|
653
|
+
if (listing.has(id)) continue;
|
|
654
|
+
if (this.forgetWorktreeId(id) !== true) changed = true;
|
|
655
|
+
}
|
|
656
|
+
const reads = await Promise.all([...listing].filter((id) => mode !== "rescan" || !previous.has(id) && !this._worktreeIds.has(id)).map(async (id) => {
|
|
657
|
+
const [ignored, known] = await this.readWorktreeId(id);
|
|
658
|
+
if (ignored != null || mode !== "rescan" || this._disposed) return [id, [ignored, known]];
|
|
659
|
+
return [id, [await this.waitForWorktreeRetry(id), void 0]];
|
|
660
|
+
}));
|
|
661
|
+
if (this._disposed || mode === "baseline") return;
|
|
662
|
+
const read = new Map(reads);
|
|
663
|
+
for (const id of listing) {
|
|
664
|
+
const result = read.get(id);
|
|
665
|
+
if (!previous.has(id)) {
|
|
666
|
+
if ((result != null ? result[0] : this._worktreeIds.get(id)) !== true) changed = true;
|
|
667
|
+
} else if (result?.[0] != null && result[1] != null && result[0] !== result[1]) changed = true;
|
|
668
|
+
}
|
|
669
|
+
if (changed) this.deliverWorktreesChange();
|
|
670
|
+
}
|
|
671
|
+
deliverWorktreesChange() {
|
|
672
|
+
for (const entry of this._sessions.values()) entry.hooks.onRepoChanged(entry.repoPath, ["worktrees", "worktreeHead"], void 0);
|
|
673
|
+
}
|
|
299
674
|
onConfigReported() {
|
|
300
675
|
if (this._configBaselining) {
|
|
301
676
|
this.deliverConfigChange(void 0);
|
|
@@ -343,14 +718,26 @@ var WatchGroup = class {
|
|
|
343
718
|
}
|
|
344
719
|
/** `undefined` when the file couldn't be read or parsed, or the read didn't settle in time */
|
|
345
720
|
async readConfig() {
|
|
721
|
+
const data = await this.readWithTimeout(joinPaths(this.commonGitDir, "config"));
|
|
722
|
+
try {
|
|
723
|
+
return data != null ? parseGitConfigBytes(data) : void 0;
|
|
724
|
+
} catch {
|
|
725
|
+
return;
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
/** `undefined` when the file couldn't be read, or the read didn't settle in time */
|
|
729
|
+
readWithTimeout(path) {
|
|
346
730
|
const readFile = this.readFile;
|
|
347
|
-
if (readFile == null) return void 0;
|
|
731
|
+
if (readFile == null) return Promise.resolve(void 0);
|
|
732
|
+
return this.withTimeout(() => readFile(path));
|
|
733
|
+
}
|
|
734
|
+
/** `undefined` when `read` rejects, or doesn't settle in time */
|
|
735
|
+
async withTimeout(read) {
|
|
348
736
|
let timer;
|
|
349
737
|
try {
|
|
350
|
-
|
|
351
|
-
timer = setTimeout(resolve,
|
|
738
|
+
return await Promise.race([read(), new Promise((resolve) => {
|
|
739
|
+
timer = setTimeout(resolve, readTimeoutMs, void 0);
|
|
352
740
|
})]);
|
|
353
|
-
return data != null ? parseGitConfigBytes(data) : void 0;
|
|
354
741
|
} catch {
|
|
355
742
|
return;
|
|
356
743
|
} finally {
|
|
@@ -366,6 +753,24 @@ var WatchGroup = class {
|
|
|
366
753
|
else entry.hooks.onRepoChanged(entry.repoPath, ["config", "remotes"], false);
|
|
367
754
|
}
|
|
368
755
|
};
|
|
756
|
+
/**
|
|
757
|
+
* `changes` with `worktreeHead` for a report that can stand for an entry's HEAD created or deleted with it: the
|
|
758
|
+
* `worktrees` directory, an entry created or deleted, or its `gitdir` deleted. A watcher may coalesce the subtree a
|
|
759
|
+
* removal deletes into one report of its parent, so no report of the HEAD comes. An entry's directory merely changed
|
|
760
|
+
* stays `worktrees` alone: a watcher may report it beside each file in it, a lock's `locked` included
|
|
761
|
+
*/
|
|
762
|
+
function withWorktreeEntryHead(changes, relativePath, reason) {
|
|
763
|
+
if (changes?.length !== 1 || changes[0] !== "worktrees") return changes;
|
|
764
|
+
if (relativePath === "worktrees") return ["worktrees", "worktreeHead"];
|
|
765
|
+
const idEnd = relativePath.indexOf("/", 10);
|
|
766
|
+
if (idEnd === -1) return reason === "create" || reason === "delete" ? ["worktrees", "worktreeHead"] : changes;
|
|
767
|
+
return reason === "delete" && relativePath.substring(idEnd + 1) === "gitdir" ? ["worktrees", "worktreeHead"] : changes;
|
|
768
|
+
}
|
|
769
|
+
/** The path doesn't exist: Node's error, or the one VS Code's file system raises */
|
|
770
|
+
function isNotFound(ex) {
|
|
771
|
+
const code = ex?.code;
|
|
772
|
+
return code === "ENOENT" || code === "FileNotFound";
|
|
773
|
+
}
|
|
369
774
|
/** The shared `.git/config` (or the lock git writes it through), relative to the common git dir */
|
|
370
775
|
function isConfigFile(relativePath) {
|
|
371
776
|
return relativePath === "config" || relativePath === "config.lock";
|