@gitkraken/core-gitlens 0.10.7 → 0.11.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 +26 -1
- package/dist/git/cache.d.ts +34 -2
- package/dist/git/cache.js +53 -7
- package/dist/git/cache.js.map +1 -1
- package/dist/git/context.d.ts +1 -1
- package/dist/git/models/repository.d.ts +3 -2
- package/dist/git/models/repository.js +5 -4
- package/dist/git/models/repository.js.map +1 -1
- package/dist/git/providers/branches.d.ts +22 -0
- package/dist/git/providers/config.d.ts +3 -1
- package/dist/git/providers/provider.d.ts +13 -1
- package/dist/git/providers/staging.d.ts +1 -1
- package/dist/git/service.d.ts +17 -1
- package/dist/git/service.js +59 -8
- package/dist/git/service.js.map +1 -1
- package/dist/git/watching/changeEvent.d.ts +48 -5
- package/dist/git/watching/changeEvent.js +82 -7
- package/dist/git/watching/changeEvent.js.map +1 -1
- package/dist/git/watching/classifyChange.d.ts +13 -2
- package/dist/git/watching/classifyChange.js +40 -5
- package/dist/git/watching/classifyChange.js.map +1 -1
- package/dist/git/watching/gitIgnoreFilter.d.ts +57 -10
- package/dist/git/watching/gitIgnoreFilter.js +186 -29
- package/dist/git/watching/gitIgnoreFilter.js.map +1 -1
- package/dist/git/watching/initWatcher.d.ts +5 -1
- package/dist/git/watching/initWatcher.js +1 -1
- package/dist/git/watching/initWatcher.js.map +1 -1
- package/dist/git/watching/provider.d.ts +33 -8
- package/dist/git/watching/watchGroup.d.ts +13 -2
- package/dist/git/watching/watchGroup.js +60 -14
- package/dist/git/watching/watchGroup.js.map +1 -1
- package/dist/git/watching/watchService.d.ts +42 -1
- package/dist/git/watching/watchService.js +172 -20
- package/dist/git/watching/watchService.js.map +1 -1
- package/dist/git/watching/watchSession.d.ts +50 -17
- package/dist/git/watching/watchSession.js +274 -83
- package/dist/git/watching/watchSession.js.map +1 -1
- package/dist/git/watching/watcherPatterns.d.ts +13 -2
- package/dist/git/watching/watcherPatterns.js +22 -2
- package/dist/git/watching/watcherPatterns.js.map +1 -1
- package/dist/git/watching/worktreeSummary.d.ts +121 -0
- package/dist/git/watching/worktreeSummary.js +427 -0
- package/dist/git/watching/worktreeSummary.js.map +1 -0
- package/dist/git-cli/cliGitProvider.d.ts +6 -1
- package/dist/git-cli/cliGitProvider.js +87 -2
- package/dist/git-cli/cliGitProvider.js.map +1 -1
- package/dist/git-cli/providers/blame.js +1 -1
- package/dist/git-cli/providers/branches.d.ts +32 -1
- package/dist/git-cli/providers/branches.js +195 -0
- package/dist/git-cli/providers/branches.js.map +1 -1
- package/dist/git-cli/providers/commits.js +1 -1
- package/dist/git-cli/providers/config.d.ts +3 -0
- package/dist/git-cli/providers/config.js +12 -6
- package/dist/git-cli/providers/config.js.map +1 -1
- package/dist/git-cli/providers/contributors.js +1 -1
- package/dist/git-cli/providers/graph.js +1 -1
- package/dist/git-cli/providers/maintenance.js +1 -1
- package/dist/git-cli/providers/refs.js +1 -1
- package/dist/git-cli/providers/staging.js +1 -1
- package/dist/git-cli/providers/status.js +1 -1
- package/dist/git-cli/service.js +1 -0
- package/dist/git-cli/service.js.map +1 -1
- package/dist/ipc/ipcServer.js +1 -1
- package/dist/plus/agents/providers/gkAgentProvider.js +2 -2
- package/dist/plus/git-github/api/github.d.ts +1 -1
- package/dist/plus/git-github/api/github.js +1 -1
- package/dist/plus/git-github/providers/github/blame.js +1 -1
- package/dist/plus/integrations/authentication/integrationAuthenticationProvider.js +1 -1
- package/dist/plus/integrations/context.d.ts +1 -1
- package/dist/plus/integrations/integrationService.js +1 -1
- package/dist/plus/integrations/models/integration.js +1 -1
- package/dist/plus/integrations/providers/bitbucket-server.d.ts +1 -1
- package/dist/utils/event.d.ts +1 -1
- package/package.json +1 -1
|
@@ -1,33 +1,57 @@
|
|
|
1
1
|
import { getScopedCounter } from "../../utils/counter.js";
|
|
2
|
+
import { getSettledValue } from "../../utils/promise.js";
|
|
3
|
+
import { isLinux } from "../../utils/platform.js";
|
|
2
4
|
import { fileUri } from "../../utils/uri.js";
|
|
3
5
|
import ignore from "ignore";
|
|
4
6
|
//#region ../git/src/watching/gitIgnoreFilter.ts
|
|
5
7
|
/**
|
|
8
|
+
* Case sensitivity when `core.ignorecase` is unset or unreadable. `ignore` matches case-insensitively by default, but
|
|
9
|
+
* git follows `core.ignorecase` — false on Linux (and on a case-sensitive macOS volume), where matching `Build/`
|
|
10
|
+
* against `build/` would call a real change ignored and drop it.
|
|
11
|
+
*/
|
|
12
|
+
const defaultIgnoreCase = !isLinux;
|
|
13
|
+
/**
|
|
6
14
|
* In-memory gitignore filter using the `ignore` npm package.
|
|
7
15
|
* Fully library-owned — no VS Code dependencies.
|
|
8
16
|
*
|
|
9
|
-
* Loads patterns eagerly on creation
|
|
10
|
-
* 1.
|
|
11
|
-
* 2.
|
|
12
|
-
* 3.
|
|
17
|
+
* Loads patterns eagerly on creation, in git's precedence order (lowest first):
|
|
18
|
+
* 1. Global excludes file (`core.excludesFile`)
|
|
19
|
+
* 2. `info/exclude`
|
|
20
|
+
* 3. `<repoPath>/.gitignore`
|
|
21
|
+
* 4. Nested `.gitignore` files (via `getGitIgnorePaths`), deeper overriding shallower
|
|
13
22
|
*
|
|
14
23
|
* Once loaded, all checks are **synchronous** for maximum performance.
|
|
15
24
|
* Call `ready()` before first use to ensure patterns are loaded.
|
|
16
25
|
*/
|
|
17
26
|
var GitIgnoreFilter = class {
|
|
18
|
-
ig = ignore();
|
|
27
|
+
ig = ignore({ ignorecase: defaultIgnoreCase });
|
|
28
|
+
/** Matchers for nested `.gitignore` files, keyed by their repo-relative directory (no trailing slash) */
|
|
29
|
+
nested;
|
|
30
|
+
/** The newest load, which {@link ready} waits for; cleared once it settles */
|
|
19
31
|
loading;
|
|
32
|
+
/** The load running now; at most one runs at a time */
|
|
33
|
+
inflight;
|
|
34
|
+
/** The one load queued behind {@link inflight}, which every refresh made meanwhile shares */
|
|
35
|
+
trailing;
|
|
20
36
|
_generation = getScopedCounter();
|
|
37
|
+
_scratchMatchers = [];
|
|
38
|
+
_scratchBases = [];
|
|
21
39
|
repoPath;
|
|
22
40
|
gitDirPath;
|
|
23
41
|
fs;
|
|
24
42
|
getGlobalExcludesPath;
|
|
43
|
+
getInfoExcludePath;
|
|
44
|
+
getGitIgnorePaths;
|
|
45
|
+
getIgnoreCase;
|
|
25
46
|
constructor(options) {
|
|
26
47
|
this.repoPath = options.repoPath;
|
|
27
48
|
this.gitDirPath = options.gitDirPath;
|
|
28
49
|
this.fs = options.fs;
|
|
29
50
|
this.getGlobalExcludesPath = options.getGlobalExcludesPath;
|
|
30
|
-
this.
|
|
51
|
+
this.getInfoExcludePath = options.getInfoExcludePath;
|
|
52
|
+
this.getGitIgnorePaths = options.getGitIgnorePaths;
|
|
53
|
+
this.getIgnoreCase = options.getIgnoreCase;
|
|
54
|
+
this.startLoad();
|
|
31
55
|
}
|
|
32
56
|
/** Resolves when patterns are loaded. Safe to call multiple times. */
|
|
33
57
|
async ready() {
|
|
@@ -35,13 +59,15 @@ var GitIgnoreFilter = class {
|
|
|
35
59
|
}
|
|
36
60
|
/**
|
|
37
61
|
* Check if a path is ignored. Must be called after `ready()`.
|
|
38
|
-
*
|
|
62
|
+
* Errs toward "not ignored": a false "ignored" drops a real change, a false "not ignored" only costs a refresh.
|
|
63
|
+
* @param relativePath Path relative to the repository root (forward slashes; a trailing slash marks a directory)
|
|
39
64
|
* @returns true if the path should be ignored
|
|
40
65
|
*/
|
|
41
66
|
isIgnored(relativePath) {
|
|
42
67
|
if (!relativePath) return false;
|
|
43
68
|
try {
|
|
44
|
-
return this.ig.ignores(relativePath);
|
|
69
|
+
if (this.nested == null) return this.ig.ignores(relativePath);
|
|
70
|
+
return this.isIgnoredNested(relativePath, this.nested);
|
|
45
71
|
} catch {
|
|
46
72
|
return false;
|
|
47
73
|
}
|
|
@@ -51,41 +77,172 @@ var GitIgnoreFilter = class {
|
|
|
51
77
|
* Must be called after `ready()`.
|
|
52
78
|
*/
|
|
53
79
|
filter(relativePaths) {
|
|
54
|
-
return
|
|
80
|
+
return relativePaths.filter((p) => !this.isIgnored(p));
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Reload all patterns (e.g., after a .gitignore or info/exclude changes), rediscovering nested `.gitignore` files.
|
|
84
|
+
* Returns a promise that resolves when a reload started after this call is complete.
|
|
85
|
+
*
|
|
86
|
+
* Single-flight: an idle filter reloads at once, but while a reload runs, every further call shares one reload
|
|
87
|
+
* queued behind it — a checkout touching many `.gitignore` files costs two reloads, not one per file.
|
|
88
|
+
*/
|
|
89
|
+
refresh() {
|
|
90
|
+
if (this.inflight == null) return this.startLoad();
|
|
91
|
+
if (this.trailing == null) {
|
|
92
|
+
const run = () => {
|
|
93
|
+
this.trailing = void 0;
|
|
94
|
+
return this.startLoad();
|
|
95
|
+
};
|
|
96
|
+
this.trailing = this.inflight.then(run, run);
|
|
97
|
+
}
|
|
98
|
+
this.loading = this.trailing;
|
|
99
|
+
return this.trailing;
|
|
100
|
+
}
|
|
101
|
+
startLoad() {
|
|
102
|
+
const load = this.loadPatterns().finally(() => {
|
|
103
|
+
if (this.inflight === load) this.inflight = void 0;
|
|
104
|
+
if (this.loading === load) this.loading = void 0;
|
|
105
|
+
});
|
|
106
|
+
this.inflight = load;
|
|
107
|
+
this.loading = load;
|
|
108
|
+
return load;
|
|
55
109
|
}
|
|
56
110
|
/**
|
|
57
|
-
*
|
|
58
|
-
*
|
|
111
|
+
* Mirrors git's directory walk: each ancestor directory is checked shallow→deep, and the first one found ignored
|
|
112
|
+
* ignores the whole path (git never descends into it, so nothing below can re-include it). At each level, the
|
|
113
|
+
* matchers whose directory is a proper ancestor apply, and the deepest decisive one wins, falling back to the root.
|
|
59
114
|
*/
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
115
|
+
isIgnoredNested(path, nested) {
|
|
116
|
+
const matchers = this._scratchMatchers;
|
|
117
|
+
const bases = this._scratchBases;
|
|
118
|
+
try {
|
|
119
|
+
const last = path.length - 1;
|
|
120
|
+
let slash = path.indexOf("/");
|
|
121
|
+
while (slash !== -1 && slash < last) {
|
|
122
|
+
if (this.isLevelIgnored(path, slash + 1, matchers, bases)) return true;
|
|
123
|
+
const matcher = nested.get(path.slice(0, slash));
|
|
124
|
+
if (matcher != null) {
|
|
125
|
+
matchers.push(matcher);
|
|
126
|
+
bases.push(slash + 1);
|
|
127
|
+
}
|
|
128
|
+
slash = path.indexOf("/", slash + 1);
|
|
129
|
+
}
|
|
130
|
+
return this.isLevelIgnored(path, path.length, matchers, bases);
|
|
131
|
+
} finally {
|
|
132
|
+
matchers.length = 0;
|
|
133
|
+
bases.length = 0;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
/** Whether `path.slice(0, end)` is ignored, given its ancestors aren't */
|
|
137
|
+
isLevelIgnored(path, end, matchers, bases) {
|
|
138
|
+
for (let i = matchers.length - 1; i >= 0; i--) {
|
|
139
|
+
const verdict = this.testLevel(matchers[i], path.slice(bases[i], end));
|
|
140
|
+
if (verdict != null) return verdict;
|
|
141
|
+
}
|
|
142
|
+
return this.testLevel(this.ig, path.slice(0, end)) ?? false;
|
|
143
|
+
}
|
|
144
|
+
/** Returns the matcher's decisive verdict for `path`, or undefined when it has none */
|
|
145
|
+
testLevel(matcher, path) {
|
|
146
|
+
const result = matcher.test(path);
|
|
147
|
+
if (result.unignored) return false;
|
|
148
|
+
if (!result.ignored) return void 0;
|
|
149
|
+
const parentEnd = path.lastIndexOf("/", path.length - 2);
|
|
150
|
+
if (parentEnd !== -1 && matcher.test(path.slice(0, parentEnd + 1)).ignored) return false;
|
|
151
|
+
return true;
|
|
63
152
|
}
|
|
64
153
|
async loadPatterns() {
|
|
65
154
|
const id = this._generation.next();
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
this.
|
|
69
|
-
this.
|
|
70
|
-
this.
|
|
155
|
+
const [globalResult, infoResult, rootResult, nestedResult, ignoreCaseResult] = await Promise.allSettled([
|
|
156
|
+
this.readGlobalExcludes(),
|
|
157
|
+
this.readInfoExclude(),
|
|
158
|
+
this.readFile(`${this.repoPath}/.gitignore`),
|
|
159
|
+
this.loadNested(),
|
|
160
|
+
this.readIgnoreCase()
|
|
71
161
|
]);
|
|
72
162
|
if (id !== this._generation.current) return;
|
|
73
|
-
|
|
74
|
-
|
|
163
|
+
const options = { ignorecase: getSettledValue(ignoreCaseResult) ?? defaultIgnoreCase };
|
|
164
|
+
const ig = ignore(options);
|
|
165
|
+
for (const content of [
|
|
166
|
+
getSettledValue(globalResult),
|
|
167
|
+
getSettledValue(infoResult),
|
|
168
|
+
getSettledValue(rootResult)
|
|
169
|
+
]) {
|
|
170
|
+
if (!content) continue;
|
|
171
|
+
try {
|
|
172
|
+
ig.add(content);
|
|
173
|
+
} catch {}
|
|
174
|
+
}
|
|
175
|
+
let nested;
|
|
176
|
+
const nestedContents = getSettledValue(nestedResult);
|
|
177
|
+
if (nestedContents?.size) {
|
|
178
|
+
nested = /* @__PURE__ */ new Map();
|
|
179
|
+
for (const [dir, content] of nestedContents) try {
|
|
180
|
+
nested.set(dir, ignore(options).add(content));
|
|
181
|
+
} catch {}
|
|
182
|
+
}
|
|
183
|
+
this.ig = ig;
|
|
184
|
+
this.nested = nested?.size ? nested : void 0;
|
|
185
|
+
}
|
|
186
|
+
/** Reads every nested `.gitignore`, keyed by its repo-relative directory (no trailing slash) */
|
|
187
|
+
async loadNested() {
|
|
188
|
+
if (this.getGitIgnorePaths == null) return void 0;
|
|
189
|
+
let paths;
|
|
190
|
+
try {
|
|
191
|
+
paths = await this.getGitIgnorePaths();
|
|
192
|
+
} catch {
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
const suffix = "/.gitignore";
|
|
196
|
+
const dirs = /* @__PURE__ */ new Set();
|
|
197
|
+
for (const path of paths) {
|
|
198
|
+
if (path.length <= 11 || !path.endsWith(suffix)) continue;
|
|
199
|
+
dirs.add(path.slice(0, -11));
|
|
200
|
+
}
|
|
201
|
+
if (!dirs.size) return void 0;
|
|
202
|
+
const entries = await Promise.allSettled(Array.from(dirs, async (dir) => {
|
|
203
|
+
const content = await this.readFile(`${this.repoPath}/${dir}${suffix}`);
|
|
204
|
+
return content ? [dir, content] : void 0;
|
|
205
|
+
}));
|
|
206
|
+
const nested = /* @__PURE__ */ new Map();
|
|
207
|
+
for (const entry of entries) {
|
|
208
|
+
const value = getSettledValue(entry);
|
|
209
|
+
if (value != null) nested.set(value[0], value[1]);
|
|
210
|
+
}
|
|
211
|
+
return nested;
|
|
75
212
|
}
|
|
76
|
-
async
|
|
213
|
+
async readFile(path) {
|
|
77
214
|
try {
|
|
78
215
|
const data = await this.fs.readFile(fileUri(path));
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
216
|
+
return new TextDecoder().decode(data);
|
|
217
|
+
} catch {
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
async readInfoExclude() {
|
|
222
|
+
if (this.getInfoExcludePath == null) return this.readFile(`${this.gitDirPath}/info/exclude`);
|
|
223
|
+
try {
|
|
224
|
+
const path = await this.getInfoExcludePath();
|
|
225
|
+
return path != null ? await this.readFile(path) : void 0;
|
|
226
|
+
} catch {
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
async readIgnoreCase() {
|
|
231
|
+
if (this.getIgnoreCase == null) return void 0;
|
|
232
|
+
try {
|
|
233
|
+
return await this.getIgnoreCase();
|
|
234
|
+
} catch {
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
82
237
|
}
|
|
83
|
-
async
|
|
84
|
-
if (this.getGlobalExcludesPath == null) return;
|
|
238
|
+
async readGlobalExcludes() {
|
|
239
|
+
if (this.getGlobalExcludesPath == null) return void 0;
|
|
85
240
|
try {
|
|
86
241
|
const path = await this.getGlobalExcludesPath();
|
|
87
|
-
|
|
88
|
-
} catch {
|
|
242
|
+
return path != null ? await this.readFile(path) : void 0;
|
|
243
|
+
} catch {
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
89
246
|
}
|
|
90
247
|
};
|
|
91
248
|
//#endregion
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"gitIgnoreFilter.js","names":[],"sources":["../../../../git/src/watching/gitIgnoreFilter.ts"],"sourcesContent":["import type { Ignore } from 'ignore';\nimport ignore from 'ignore';\nimport { getScopedCounter } from '@gitlens/utils/counter.js';\nimport { fileUri } from '@gitlens/utils/uri.js';\nimport type { FileSystemProvider } from '../context.js';\n\nexport interface GitIgnoreFilterOptions {\n\t/** Absolute path to the repository root (working tree) */\n\treadonly repoPath: string;\n\t/** Absolute path to the .git directory (for info/exclude) */\n\treadonly gitDirPath: string;\n\t/** File system provider for reading ignore files */\n\treadonly fs: FileSystemProvider;\n\t/**\n\t * Returns the path to the global excludes file (core.excludesFile).\n\t * May return undefined if not configured.\n\t */\n\treadonly getGlobalExcludesPath?: () => Promise<string | undefined>;\n}\n\n/**\n * In-memory gitignore filter using the `ignore` npm package.\n * Fully library-owned — no VS Code dependencies.\n *\n * Loads patterns eagerly on creation from:\n * 1. `<repoPath>/.gitignore`\n * 2. `<gitDirPath>/info/exclude`\n * 3. Global excludes file (`core.excludesFile`)\n *\n * Once loaded, all checks are **synchronous** for maximum performance.\n * Call `ready()` before first use to ensure patterns are loaded.\n */\nexport class GitIgnoreFilter {\n\tprivate ig: Ignore = ignore();\n\tprivate loading: Promise<void> | undefined;\n\tprivate readonly _generation = getScopedCounter();\n\n\tprivate readonly repoPath: string;\n\tprivate readonly gitDirPath: string;\n\tprivate readonly fs: FileSystemProvider;\n\tprivate readonly getGlobalExcludesPath?: () => Promise<string | undefined>;\n\n\tconstructor(options: GitIgnoreFilterOptions) {\n\t\tthis.repoPath = options.repoPath;\n\t\tthis.gitDirPath = options.gitDirPath;\n\t\tthis.fs = options.fs;\n\t\tthis.getGlobalExcludesPath = options.getGlobalExcludesPath;\n\n\t\t// Start loading immediately\n\t\tthis.loading = this.loadPatterns();\n\t}\n\n\t/** Resolves when patterns are loaded. Safe to call multiple times. */\n\tasync ready(): Promise<void> {\n\t\tif (this.loading != null) {\n\t\t\tawait this.loading;\n\t\t}\n\t}\n\n\t/**\n\t * Check if a path is ignored. Must be called after `ready()`.\n\t * @param relativePath Path relative to the repository root (forward slashes)\n\t * @returns true if the path should be ignored\n\t */\n\tisIgnored(relativePath: string): boolean {\n\t\tif (!relativePath) return false;\n\n\t\ttry {\n\t\t\treturn this.ig.ignores(relativePath);\n\t\t} catch {\n\t\t\t// `ignore` can throw on edge-case paths\n\t\t\treturn false;\n\t\t}\n\t}\n\n\t/**\n\t * Filter an array of relative paths, returning only non-ignored ones.\n\t * Must be called after `ready()`.\n\t */\n\tfilter(relativePaths: readonly string[]): string[] {\n\t\treturn this.ig.filter(relativePaths);\n\t}\n\n\t/**\n\t * Reload all patterns (e.g., after .gitignore or info/exclude changes).\n\t * Returns a promise that resolves when reload is complete.\n\t */\n\tasync refresh(): Promise<void> {\n\t\tthis.loading = this.loadPatterns();\n\t\tawait this.loading;\n\t}\n\n\tprivate async loadPatterns(): Promise<void> {\n\t\tconst id = this._generation.next();\n\t\tconst newIg = ignore();\n\n\t\tawait Promise.allSettled([\n\t\t\tthis.loadFile(newIg, `${this.repoPath}/.gitignore`),\n\t\t\tthis.loadFile(newIg, `${this.gitDirPath}/info/exclude`),\n\t\t\tthis.loadGlobalExcludes(newIg),\n\t\t]);\n\n\t\t// Only commit results if no newer refresh has started\n\t\tif (id !== this._generation.current) return;\n\n\t\tthis.ig = newIg;\n\t\tthis.loading = undefined;\n\t}\n\n\tprivate async loadFile(target: Ignore, path: string): Promise<void> {\n\t\ttry {\n\t\t\tconst data = await this.fs.readFile(fileUri(path));\n\t\t\tconst content = new TextDecoder().decode(data);\n\t\t\tif (content) {\n\t\t\t\ttarget.add(content);\n\t\t\t}\n\t\t} catch {\n\t\t\t// File might not exist or be unreadable — that's fine\n\t\t}\n\t}\n\n\tprivate async loadGlobalExcludes(target: Ignore): Promise<void> {\n\t\tif (this.getGlobalExcludesPath == null) return;\n\n\t\ttry {\n\t\t\tconst path = await this.getGlobalExcludesPath();\n\t\t\tif (path != null) {\n\t\t\t\tawait this.loadFile(target, path);\n\t\t\t}\n\t\t} catch {\n\t\t\t// Global excludes might not be configured\n\t\t}\n\t}\n}\n"],"mappings":";;;;;;;;;;;;;;;;AAgCA,IAAa,kBAAb,MAA6B;CAC5B,KAAqB,OAAO;CAC5B;CACA,cAA+B,iBAAiB;CAEhD;CACA;CACA;CACA;CAEA,YAAY,SAAiC;EAC5C,KAAK,WAAW,QAAQ;EACxB,KAAK,aAAa,QAAQ;EAC1B,KAAK,KAAK,QAAQ;EAClB,KAAK,wBAAwB,QAAQ;EAGrC,KAAK,UAAU,KAAK,aAAa;CAClC;;CAGA,MAAM,QAAuB;EAC5B,IAAI,KAAK,WAAW,MACnB,MAAM,KAAK;CAEb;;;;;;CAOA,UAAU,cAA+B;EACxC,IAAI,CAAC,cAAc,OAAO;EAE1B,IAAI;GACH,OAAO,KAAK,GAAG,QAAQ,YAAY;EACpC,QAAQ;GAEP,OAAO;EACR;CACD;;;;;CAMA,OAAO,eAA4C;EAClD,OAAO,KAAK,GAAG,OAAO,aAAa;CACpC;;;;;CAMA,MAAM,UAAyB;EAC9B,KAAK,UAAU,KAAK,aAAa;EACjC,MAAM,KAAK;CACZ;CAEA,MAAc,eAA8B;EAC3C,MAAM,KAAK,KAAK,YAAY,KAAK;EACjC,MAAM,QAAQ,OAAO;EAErB,MAAM,QAAQ,WAAW;GACxB,KAAK,SAAS,OAAO,GAAG,KAAK,SAAS,YAAY;GAClD,KAAK,SAAS,OAAO,GAAG,KAAK,WAAW,cAAc;GACtD,KAAK,mBAAmB,KAAK;EAC9B,CAAC;EAGD,IAAI,OAAO,KAAK,YAAY,SAAS;EAErC,KAAK,KAAK;EACV,KAAK,UAAU,KAAA;CAChB;CAEA,MAAc,SAAS,QAAgB,MAA6B;EACnE,IAAI;GACH,MAAM,OAAO,MAAM,KAAK,GAAG,SAAS,QAAQ,IAAI,CAAC;GACjD,MAAM,UAAU,IAAI,YAAY,CAAC,CAAC,OAAO,IAAI;GAC7C,IAAI,SACH,OAAO,IAAI,OAAO;EAEpB,QAAQ,CAER;CACD;CAEA,MAAc,mBAAmB,QAA+B;EAC/D,IAAI,KAAK,yBAAyB,MAAM;EAExC,IAAI;GACH,MAAM,OAAO,MAAM,KAAK,sBAAsB;GAC9C,IAAI,QAAQ,MACX,MAAM,KAAK,SAAS,QAAQ,IAAI;EAElC,QAAQ,CAER;CACD;AACD"}
|
|
1
|
+
{"version":3,"file":"gitIgnoreFilter.js","names":[],"sources":["../../../../git/src/watching/gitIgnoreFilter.ts"],"sourcesContent":["import type { Ignore } from 'ignore';\nimport ignore from 'ignore';\nimport { getScopedCounter } from '@gitlens/utils/counter.js';\nimport { isLinux } from '@gitlens/utils/platform.js';\nimport { getSettledValue } from '@gitlens/utils/promise.js';\nimport { fileUri } from '@gitlens/utils/uri.js';\nimport type { FileSystemProvider } from '../context.js';\n\n/**\n * Case sensitivity when `core.ignorecase` is unset or unreadable. `ignore` matches case-insensitively by default, but\n * git follows `core.ignorecase` — false on Linux (and on a case-sensitive macOS volume), where matching `Build/`\n * against `build/` would call a real change ignored and drop it.\n */\nconst defaultIgnoreCase = !isLinux;\n\nexport interface GitIgnoreFilterOptions {\n\t/** Absolute path to the repository root (working tree) */\n\treadonly repoPath: string;\n\t/** Absolute path to the .git directory (for info/exclude when `getInfoExcludePath` isn't provided) */\n\treadonly gitDirPath: string;\n\t/** File system provider for reading ignore files */\n\treadonly fs: FileSystemProvider;\n\t/**\n\t * Returns the path to the global excludes file (core.excludesFile).\n\t * May return undefined if not configured.\n\t */\n\treadonly getGlobalExcludesPath?: () => Promise<string | undefined>;\n\t/**\n\t * Returns the absolute path to `info/exclude`, which lives in the COMMON git dir for a linked worktree.\n\t * When provided, replaces the `gitDirPath`-based resolution; undefined means there is none to read.\n\t */\n\treadonly getInfoExcludePath?: () => Promise<string | undefined>;\n\t/**\n\t * Returns the repo-relative, `/`-separated paths of every `.gitignore` git reads (root included).\n\t * Without it, only the root `.gitignore` is honoured.\n\t */\n\treadonly getGitIgnorePaths?: () => Promise<readonly string[]>;\n\t/**\n\t * Returns `core.ignorecase`, which decides whether patterns match case-insensitively. Undefined (or a failed read)\n\t * falls back to the platform default (case-sensitive on Linux only).\n\t */\n\treadonly getIgnoreCase?: () => Promise<boolean | undefined>;\n}\n\n/**\n * In-memory gitignore filter using the `ignore` npm package.\n * Fully library-owned — no VS Code dependencies.\n *\n * Loads patterns eagerly on creation, in git's precedence order (lowest first):\n * 1. Global excludes file (`core.excludesFile`)\n * 2. `info/exclude`\n * 3. `<repoPath>/.gitignore`\n * 4. Nested `.gitignore` files (via `getGitIgnorePaths`), deeper overriding shallower\n *\n * Once loaded, all checks are **synchronous** for maximum performance.\n * Call `ready()` before first use to ensure patterns are loaded.\n */\nexport class GitIgnoreFilter {\n\tprivate ig: Ignore = ignore({ ignorecase: defaultIgnoreCase });\n\t/** Matchers for nested `.gitignore` files, keyed by their repo-relative directory (no trailing slash) */\n\tprivate nested: Map<string, Ignore> | undefined;\n\t/** The newest load, which {@link ready} waits for; cleared once it settles */\n\tprivate loading: Promise<void> | undefined;\n\t/** The load running now; at most one runs at a time */\n\tprivate inflight: Promise<void> | undefined;\n\t/** The one load queued behind {@link inflight}, which every refresh made meanwhile shares */\n\tprivate trailing: Promise<void> | undefined;\n\tprivate readonly _generation = getScopedCounter();\n\n\t// Reused across `isIgnored` calls (which are synchronous, so never interleave) to avoid per-event allocations\n\tprivate readonly _scratchMatchers: Ignore[] = [];\n\tprivate readonly _scratchBases: number[] = [];\n\n\tprivate readonly repoPath: string;\n\tprivate readonly gitDirPath: string;\n\tprivate readonly fs: FileSystemProvider;\n\tprivate readonly getGlobalExcludesPath?: () => Promise<string | undefined>;\n\tprivate readonly getInfoExcludePath?: () => Promise<string | undefined>;\n\tprivate readonly getGitIgnorePaths?: () => Promise<readonly string[]>;\n\tprivate readonly getIgnoreCase?: () => Promise<boolean | undefined>;\n\n\tconstructor(options: GitIgnoreFilterOptions) {\n\t\tthis.repoPath = options.repoPath;\n\t\tthis.gitDirPath = options.gitDirPath;\n\t\tthis.fs = options.fs;\n\t\tthis.getGlobalExcludesPath = options.getGlobalExcludesPath;\n\t\tthis.getInfoExcludePath = options.getInfoExcludePath;\n\t\tthis.getGitIgnorePaths = options.getGitIgnorePaths;\n\t\tthis.getIgnoreCase = options.getIgnoreCase;\n\n\t\t// Start loading immediately\n\t\tvoid this.startLoad();\n\t}\n\n\t/** Resolves when patterns are loaded. Safe to call multiple times. */\n\tasync ready(): Promise<void> {\n\t\tif (this.loading != null) {\n\t\t\tawait this.loading;\n\t\t}\n\t}\n\n\t/**\n\t * Check if a path is ignored. Must be called after `ready()`.\n\t * Errs toward \"not ignored\": a false \"ignored\" drops a real change, a false \"not ignored\" only costs a refresh.\n\t * @param relativePath Path relative to the repository root (forward slashes; a trailing slash marks a directory)\n\t * @returns true if the path should be ignored\n\t */\n\tisIgnored(relativePath: string): boolean {\n\t\tif (!relativePath) return false;\n\n\t\ttry {\n\t\t\tif (this.nested == null) return this.ig.ignores(relativePath);\n\n\t\t\treturn this.isIgnoredNested(relativePath, this.nested);\n\t\t} catch {\n\t\t\t// `ignore` can throw on edge-case paths\n\t\t\treturn false;\n\t\t}\n\t}\n\n\t/**\n\t * Filter an array of relative paths, returning only non-ignored ones.\n\t * Must be called after `ready()`.\n\t */\n\tfilter(relativePaths: readonly string[]): string[] {\n\t\treturn relativePaths.filter(p => !this.isIgnored(p));\n\t}\n\n\t/**\n\t * Reload all patterns (e.g., after a .gitignore or info/exclude changes), rediscovering nested `.gitignore` files.\n\t * Returns a promise that resolves when a reload started after this call is complete.\n\t *\n\t * Single-flight: an idle filter reloads at once, but while a reload runs, every further call shares one reload\n\t * queued behind it — a checkout touching many `.gitignore` files costs two reloads, not one per file.\n\t */\n\trefresh(): Promise<void> {\n\t\tif (this.inflight == null) return this.startLoad();\n\n\t\tif (this.trailing == null) {\n\t\t\tconst run = (): Promise<void> => {\n\t\t\t\tthis.trailing = undefined;\n\t\t\t\treturn this.startLoad();\n\t\t\t};\n\t\t\tthis.trailing = this.inflight.then(run, run);\n\t\t}\n\t\tthis.loading = this.trailing;\n\t\treturn this.trailing;\n\t}\n\n\tprivate startLoad(): Promise<void> {\n\t\tconst load: Promise<void> = this.loadPatterns().finally(() => {\n\t\t\tif (this.inflight === load) {\n\t\t\t\tthis.inflight = undefined;\n\t\t\t}\n\t\t\tif (this.loading === load) {\n\t\t\t\tthis.loading = undefined;\n\t\t\t}\n\t\t});\n\t\tthis.inflight = load;\n\t\tthis.loading = load;\n\t\treturn load;\n\t}\n\n\t/**\n\t * Mirrors git's directory walk: each ancestor directory is checked shallow→deep, and the first one found ignored\n\t * ignores the whole path (git never descends into it, so nothing below can re-include it). At each level, the\n\t * matchers whose directory is a proper ancestor apply, and the deepest decisive one wins, falling back to the root.\n\t */\n\tprivate isIgnoredNested(path: string, nested: Map<string, Ignore>): boolean {\n\t\tconst matchers = this._scratchMatchers;\n\t\tconst bases = this._scratchBases;\n\n\t\ttry {\n\t\t\t// A trailing slash belongs to the path itself (a directory form), not to an ancestor\n\t\t\tconst last = path.length - 1;\n\n\t\t\tlet slash = path.indexOf('/');\n\t\t\twhile (slash !== -1 && slash < last) {\n\t\t\t\tif (this.isLevelIgnored(path, slash + 1, matchers, bases)) return true;\n\n\t\t\t\t// A directory's own `.gitignore` applies below it, never to the directory itself\n\t\t\t\tconst matcher = nested.get(path.slice(0, slash));\n\t\t\t\tif (matcher != null) {\n\t\t\t\t\tmatchers.push(matcher);\n\t\t\t\t\tbases.push(slash + 1);\n\t\t\t\t}\n\n\t\t\t\tslash = path.indexOf('/', slash + 1);\n\t\t\t}\n\n\t\t\treturn this.isLevelIgnored(path, path.length, matchers, bases);\n\t\t} finally {\n\t\t\tmatchers.length = 0;\n\t\t\tbases.length = 0;\n\t\t}\n\t}\n\n\t/** Whether `path.slice(0, end)` is ignored, given its ancestors aren't */\n\tprivate isLevelIgnored(path: string, end: number, matchers: readonly Ignore[], bases: readonly number[]): boolean {\n\t\tfor (let i = matchers.length - 1; i >= 0; i--) {\n\t\t\tconst verdict = this.testLevel(matchers[i], path.slice(bases[i], end));\n\t\t\tif (verdict != null) return verdict;\n\t\t}\n\n\t\treturn this.testLevel(this.ig, path.slice(0, end)) ?? false;\n\t}\n\n\t/** Returns the matcher's decisive verdict for `path`, or undefined when it has none */\n\tprivate testLevel(matcher: Ignore, path: string): boolean | undefined {\n\t\tconst result = matcher.test(path);\n\t\tif (result.unignored) return false;\n\t\tif (!result.ignored) return undefined;\n\n\t\t// `ignore` folds its own verdict for each ancestor into a path's result: once the matcher considers a parent\n\t\t// ignored, every child reports that parent's result. Reaching here means a deeper matcher re-included that\n\t\t// parent, so this result says nothing about the path itself — the matcher's own rules are unknowable, so\n\t\t// answer \"not ignored\" rather than risk dropping a real change.\n\t\tconst parentEnd = path.lastIndexOf('/', path.length - 2);\n\t\tif (parentEnd !== -1 && matcher.test(path.slice(0, parentEnd + 1)).ignored) return false;\n\n\t\treturn true;\n\t}\n\n\tprivate async loadPatterns(): Promise<void> {\n\t\tconst id = this._generation.next();\n\n\t\tconst [globalResult, infoResult, rootResult, nestedResult, ignoreCaseResult] = await Promise.allSettled([\n\t\t\tthis.readGlobalExcludes(),\n\t\t\tthis.readInfoExclude(),\n\t\t\tthis.readFile(`${this.repoPath}/.gitignore`),\n\t\t\tthis.loadNested(),\n\t\t\tthis.readIgnoreCase(),\n\t\t]);\n\n\t\t// Only commit results if no newer refresh has started\n\t\tif (id !== this._generation.current) return;\n\n\t\tconst options = { ignorecase: getSettledValue(ignoreCaseResult) ?? defaultIgnoreCase };\n\n\t\t// Added in git's precedence order (lowest first), since the last matching pattern wins\n\t\tconst ig = ignore(options);\n\t\tfor (const content of [\n\t\t\tgetSettledValue(globalResult),\n\t\t\tgetSettledValue(infoResult),\n\t\t\tgetSettledValue(rootResult),\n\t\t]) {\n\t\t\tif (!content) continue;\n\n\t\t\ttry {\n\t\t\t\tig.add(content);\n\t\t\t} catch {\n\t\t\t\t// A source `ignore` can't parse is skipped rather than failing the whole load\n\t\t\t}\n\t\t}\n\n\t\tlet nested: Map<string, Ignore> | undefined;\n\t\tconst nestedContents = getSettledValue(nestedResult);\n\t\tif (nestedContents?.size) {\n\t\t\tnested = new Map();\n\t\t\tfor (const [dir, content] of nestedContents) {\n\t\t\t\ttry {\n\t\t\t\t\tnested.set(dir, ignore(options).add(content));\n\t\t\t\t} catch {\n\t\t\t\t\t// A file `ignore` can't parse is skipped rather than failing the whole load\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tthis.ig = ig;\n\t\tthis.nested = nested?.size ? nested : undefined;\n\t}\n\n\t/** Reads every nested `.gitignore`, keyed by its repo-relative directory (no trailing slash) */\n\tprivate async loadNested(): Promise<Map<string, string> | undefined> {\n\t\tif (this.getGitIgnorePaths == null) return undefined;\n\n\t\tlet paths: readonly string[];\n\t\ttry {\n\t\t\tpaths = await this.getGitIgnorePaths();\n\t\t} catch {\n\t\t\t// Discovery failed — fall back to the root `.gitignore` alone\n\t\t\treturn undefined;\n\t\t}\n\n\t\tconst suffix = '/.gitignore';\n\t\tconst dirs = new Set<string>();\n\t\tfor (const path of paths) {\n\t\t\t// The root `.gitignore` (no directory) is loaded with the other root sources\n\t\t\tif (path.length <= suffix.length || !path.endsWith(suffix)) continue;\n\n\t\t\tdirs.add(path.slice(0, -suffix.length));\n\t\t}\n\t\tif (!dirs.size) return undefined;\n\n\t\tconst entries = await Promise.allSettled(\n\t\t\tArray.from(dirs, async dir => {\n\t\t\t\tconst content = await this.readFile(`${this.repoPath}/${dir}${suffix}`);\n\t\t\t\treturn content ? ([dir, content] as const) : undefined;\n\t\t\t}),\n\t\t);\n\n\t\tconst nested = new Map<string, string>();\n\t\tfor (const entry of entries) {\n\t\t\tconst value = getSettledValue(entry);\n\t\t\tif (value != null) {\n\t\t\t\tnested.set(value[0], value[1]);\n\t\t\t}\n\t\t}\n\t\treturn nested;\n\t}\n\n\tprivate async readFile(path: string): Promise<string | undefined> {\n\t\ttry {\n\t\t\tconst data = await this.fs.readFile(fileUri(path));\n\t\t\treturn new TextDecoder().decode(data);\n\t\t} catch {\n\t\t\t// File might not exist or be unreadable — that's fine\n\t\t\treturn undefined;\n\t\t}\n\t}\n\n\tprivate async readInfoExclude(): Promise<string | undefined> {\n\t\tif (this.getInfoExcludePath == null) return this.readFile(`${this.gitDirPath}/info/exclude`);\n\n\t\ttry {\n\t\t\tconst path = await this.getInfoExcludePath();\n\t\t\treturn path != null ? await this.readFile(path) : undefined;\n\t\t} catch {\n\t\t\t// The git dir couldn't be resolved\n\t\t\treturn undefined;\n\t\t}\n\t}\n\n\tprivate async readIgnoreCase(): Promise<boolean | undefined> {\n\t\tif (this.getIgnoreCase == null) return undefined;\n\n\t\ttry {\n\t\t\treturn await this.getIgnoreCase();\n\t\t} catch {\n\t\t\t// Unreadable — the platform default applies\n\t\t\treturn undefined;\n\t\t}\n\t}\n\n\tprivate async readGlobalExcludes(): Promise<string | undefined> {\n\t\tif (this.getGlobalExcludesPath == null) return undefined;\n\n\t\ttry {\n\t\t\tconst path = await this.getGlobalExcludesPath();\n\t\t\treturn path != null ? await this.readFile(path) : undefined;\n\t\t} catch {\n\t\t\t// Global excludes might not be configured\n\t\t\treturn undefined;\n\t\t}\n\t}\n}\n"],"mappings":"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"}
|
|
@@ -3,7 +3,11 @@ import { Event } from "../../utils/event.js";
|
|
|
3
3
|
import { FileWatchingProvider } from "./provider.js";
|
|
4
4
|
//#region ../git/src/watching/initWatcher.d.ts
|
|
5
5
|
export interface RepositoryInitEvent {
|
|
6
|
-
/**
|
|
6
|
+
/**
|
|
7
|
+
* Absolute path of the created .git directory. After the watcher lost events (`overflow`), it is instead the base
|
|
8
|
+
* path or the subtree whose events were dropped — a `.git` may have been created anywhere beneath it, so the host
|
|
9
|
+
* rescans it for repositories.
|
|
10
|
+
*/
|
|
7
11
|
readonly path: string;
|
|
8
12
|
/** Absolute path of the base directory being watched */
|
|
9
13
|
readonly basePath: string;
|
|
@@ -38,7 +38,7 @@ var RepositoryInitWatcher = class {
|
|
|
38
38
|
watch(basePath) {
|
|
39
39
|
if (this._disposed || this.watchers.has(basePath)) return createDisposable(() => {});
|
|
40
40
|
const watcher = this.provider.createWatcher(basePath, gitInitGlob, (event) => {
|
|
41
|
-
if (event.reason !== "create") return;
|
|
41
|
+
if (event.reason !== "create" && event.reason !== "overflow") return;
|
|
42
42
|
this.emitter.fire({
|
|
43
43
|
path: event.path,
|
|
44
44
|
basePath
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"initWatcher.js","names":[],"sources":["../../../../git/src/watching/initWatcher.ts"],"sourcesContent":["import type { UnifiedDisposable } from '@gitlens/utils/disposable.js';\nimport { createDisposable } from '@gitlens/utils/disposable.js';\nimport type { Event } from '@gitlens/utils/event.js';\nimport { Emitter } from '@gitlens/utils/event.js';\nimport type { FileWatcher, FileWatchingProvider } from './provider.js';\nimport { gitInitGlob } from './watcherPatterns.js';\n\nexport interface RepositoryInitEvent {\n\t
|
|
1
|
+
{"version":3,"file":"initWatcher.js","names":[],"sources":["../../../../git/src/watching/initWatcher.ts"],"sourcesContent":["import type { UnifiedDisposable } from '@gitlens/utils/disposable.js';\nimport { createDisposable } from '@gitlens/utils/disposable.js';\nimport type { Event } from '@gitlens/utils/event.js';\nimport { Emitter } from '@gitlens/utils/event.js';\nimport type { FileWatcher, FileWatchingProvider } from './provider.js';\nimport { gitInitGlob } from './watcherPatterns.js';\n\nexport interface RepositoryInitEvent {\n\t/**\n\t * Absolute path of the created .git directory. After the watcher lost events (`overflow`), it is instead the base\n\t * path or the subtree whose events were dropped — a `.git` may have been created anywhere beneath it, so the host\n\t * rescans it for repositories.\n\t */\n\treadonly path: string;\n\t/** Absolute path of the base directory being watched */\n\treadonly basePath: string;\n}\n\n/**\n * Watches for `.git` directory creation within watched base paths.\n * Used to discover new repositories when `git init` or `git clone` runs.\n *\n * This is separate from {@link RepositoryWatchService} which monitors\n * already-discovered repositories. The init watcher is a pre-discovery\n * concern — it fires before a repository is known.\n */\nexport class RepositoryInitWatcher implements UnifiedDisposable {\n\tprivate readonly provider: FileWatchingProvider;\n\tprivate readonly emitter = new Emitter<RepositoryInitEvent>();\n\tprivate readonly watchers = new Map<string, FileWatcher>();\n\tprivate _disposed = false;\n\n\tconstructor(provider: FileWatchingProvider) {\n\t\tthis.provider = provider;\n\t}\n\n\t[Symbol.dispose](): void {\n\t\tthis.dispose();\n\t}\n\n\tdispose(): void {\n\t\tif (this._disposed) return;\n\n\t\tthis._disposed = true;\n\n\t\tfor (const watcher of this.watchers.values()) {\n\t\t\twatcher.dispose();\n\t\t}\n\t\tthis.watchers.clear();\n\t\tthis.emitter.dispose();\n\t}\n\n\tget onDidCreate(): Event<RepositoryInitEvent> {\n\t\treturn this.emitter.event;\n\t}\n\n\t/**\n\t * Start watching a directory for `.git` creation.\n\t * Returns a disposable that stops watching that directory.\n\t */\n\twatch(basePath: string): UnifiedDisposable {\n\t\tif (this._disposed || this.watchers.has(basePath)) {\n\t\t\treturn createDisposable(() => {});\n\t\t}\n\n\t\tconst watcher = this.provider.createWatcher(basePath, gitInitGlob, event => {\n\t\t\tif (event.reason !== 'create' && event.reason !== 'overflow') return;\n\n\t\t\tthis.emitter.fire({ path: event.path, basePath: basePath });\n\t\t});\n\n\t\tthis.watchers.set(basePath, watcher);\n\n\t\treturn createDisposable(\n\t\t\t() => {\n\t\t\t\tconst w = this.watchers.get(basePath);\n\t\t\t\tif (w === watcher) {\n\t\t\t\t\tthis.watchers.delete(basePath);\n\t\t\t\t\tw.dispose();\n\t\t\t\t}\n\t\t\t},\n\t\t\t{ once: true },\n\t\t);\n\t}\n}\n"],"mappings":";;;;;;;;;;;;AA0BA,IAAa,wBAAb,MAAgE;CAC/D;CACA,UAA2B,IAAI,QAA6B;CAC5D,2BAA4B,IAAI,IAAyB;CACzD,YAAoB;CAEpB,YAAY,UAAgC;EAC3C,KAAK,WAAW;CACjB;CAEA,CAAC,OAAO,WAAiB;EACxB,KAAK,QAAQ;CACd;CAEA,UAAgB;EACf,IAAI,KAAK,WAAW;EAEpB,KAAK,YAAY;EAEjB,KAAK,MAAM,WAAW,KAAK,SAAS,OAAO,GAC1C,QAAQ,QAAQ;EAEjB,KAAK,SAAS,MAAM;EACpB,KAAK,QAAQ,QAAQ;CACtB;CAEA,IAAI,cAA0C;EAC7C,OAAO,KAAK,QAAQ;CACrB;;;;;CAMA,MAAM,UAAqC;EAC1C,IAAI,KAAK,aAAa,KAAK,SAAS,IAAI,QAAQ,GAC/C,OAAO,uBAAuB,CAAC,CAAC;EAGjC,MAAM,UAAU,KAAK,SAAS,cAAc,UAAU,cAAa,UAAS;GAC3E,IAAI,MAAM,WAAW,YAAY,MAAM,WAAW,YAAY;GAE9D,KAAK,QAAQ,KAAK;IAAE,MAAM,MAAM;IAAgB;GAAS,CAAC;EAC3D,CAAC;EAED,KAAK,SAAS,IAAI,UAAU,OAAO;EAEnC,OAAO,uBACA;GACL,MAAM,IAAI,KAAK,SAAS,IAAI,QAAQ;GACpC,IAAI,MAAM,SAAS;IAClB,KAAK,SAAS,OAAO,QAAQ;IAC7B,EAAE,QAAQ;GACX;EACD,GACA,EAAE,MAAM,KAAK,CACd;CACD;AACD"}
|
|
@@ -1,24 +1,49 @@
|
|
|
1
1
|
import { UnifiedDisposable } from "../../utils/disposable.js";
|
|
2
2
|
//#region ../git/src/watching/provider.d.ts
|
|
3
|
-
/**
|
|
3
|
+
/**
|
|
4
|
+
* Event from a filesystem watcher.
|
|
5
|
+
*
|
|
6
|
+
* A reported directory path stands for everything beneath it: a watcher can miss the files of a just-created
|
|
7
|
+
* directory (it only sees the directory after its create event), but never the directory itself.
|
|
8
|
+
*/
|
|
4
9
|
export interface FileWatchEvent {
|
|
5
|
-
/** Absolute path of the changed file */
|
|
10
|
+
/** Absolute path of the changed file or directory; for `overflow`, the watcher's `basePath` or a subtree of it */
|
|
6
11
|
readonly path: string;
|
|
7
|
-
|
|
12
|
+
/**
|
|
13
|
+
* `overflow`: events under `path` (the watcher's `basePath`, or a subtree of it) were dropped, so core rescans
|
|
14
|
+
* rather than trusting what it was told. Providers that can't lose events never emit it.
|
|
15
|
+
*/
|
|
16
|
+
readonly reason: 'create' | 'change' | 'delete' | 'overflow';
|
|
8
17
|
}
|
|
9
18
|
/** Opaque token representing a filesystem watcher */
|
|
10
19
|
export interface FileWatcher extends UnifiedDisposable {}
|
|
20
|
+
export interface FileWatcherOptions {
|
|
21
|
+
/**
|
|
22
|
+
* Directories to leave unwatched, each with everything beneath it. Entries are `/`-separated and relative to
|
|
23
|
+
* `basePath`: a glob when it contains glob syntax (`build-*`), otherwise a literal path (`dist`,
|
|
24
|
+
* `packages/app/out`) — core never emits a literal containing glob metacharacters. A provider that can prune
|
|
25
|
+
* natively should (no OS watch, no events); one that can't may ignore this, since core filters events itself.
|
|
26
|
+
* (Map each entry to your backend's syntax — e.g. picomatch-based watchers need both `X` and `X/**`.)
|
|
27
|
+
*/
|
|
28
|
+
readonly excludes?: readonly string[];
|
|
29
|
+
}
|
|
11
30
|
/**
|
|
12
31
|
* Factory for creating filesystem watchers. The library calls this
|
|
13
|
-
* with a base path + glob pattern
|
|
14
|
-
* watchers (e.g., VS Code's
|
|
32
|
+
* with a base path + glob pattern (plus, optionally, directories to prune);
|
|
33
|
+
* the caller creates platform-specific watchers (e.g., VS Code's
|
|
34
|
+
* `workspace.createFileSystemWatcher`, or a native watcher such as `@parcel/watcher`).
|
|
15
35
|
*
|
|
16
36
|
* This is the only pluggable concern in the watching system.
|
|
17
|
-
* Everything else (interpretation, gitignore, debouncing,
|
|
18
|
-
* is fully library-owned.
|
|
37
|
+
* Everything else (interpretation, gitignore, debouncing, which directories
|
|
38
|
+
* to exclude, etc.) is fully library-owned.
|
|
19
39
|
*/
|
|
20
40
|
export interface FileWatchingProvider {
|
|
21
|
-
createWatcher(basePath: string, pattern: string, onEvent: (event: FileWatchEvent) => void): FileWatcher;
|
|
41
|
+
createWatcher(basePath: string, pattern: string, onEvent: (event: FileWatchEvent) => void, options?: FileWatcherOptions): FileWatcher;
|
|
42
|
+
/**
|
|
43
|
+
* True when `createWatcher` prunes `options.excludes` natively. Core only spends git reads on the git-ignored
|
|
44
|
+
* set for a provider that can use it.
|
|
45
|
+
*/
|
|
46
|
+
readonly supportsExcludes?: boolean;
|
|
22
47
|
}
|
|
23
48
|
//#endregion
|
|
24
49
|
//# sourceMappingURL=provider.d.ts.map
|
|
@@ -7,7 +7,11 @@ import { GitDir, RepositoryChange } from "../models/repository.js";
|
|
|
7
7
|
* These are raw, un-debounced events — the session handles debouncing.
|
|
8
8
|
*/
|
|
9
9
|
export interface WatchGroupHooks {
|
|
10
|
-
|
|
10
|
+
/**
|
|
11
|
+
* `ref` is the full ref name (or ref directory) or pseudo-ref (`HEAD`, `ORIG_HEAD`) whose file changed, with a
|
|
12
|
+
* trailing `.lock` stripped; `undefined` for any other `.git` file.
|
|
13
|
+
*/
|
|
14
|
+
readonly onRepoChanged: (repoPath: string, changes: RepositoryChange[], ref?: string) => void;
|
|
11
15
|
readonly onFetchHeadChanged?: (repoPath: string) => void;
|
|
12
16
|
readonly onIgnoresChanged?: (repoPath: string) => void;
|
|
13
17
|
}
|
|
@@ -19,6 +23,10 @@ interface SessionEntry {
|
|
|
19
23
|
readonly repoPath: string;
|
|
20
24
|
readonly gitDirPath: string;
|
|
21
25
|
readonly rootWatcher: FileWatcher;
|
|
26
|
+
/** True when the root watcher is on the common dir itself, so it already sees every common file */
|
|
27
|
+
readonly coversCommon: boolean;
|
|
28
|
+
/** For a linked session, its `<n>` in `<common>/worktrees/<n>`; otherwise `undefined` */
|
|
29
|
+
readonly worktreeId: string | undefined;
|
|
22
30
|
readonly hooks: WatchGroupHooks;
|
|
23
31
|
}
|
|
24
32
|
/**
|
|
@@ -26,7 +34,8 @@ interface SessionEntry {
|
|
|
26
34
|
* shared across all worktrees that reference it.
|
|
27
35
|
*
|
|
28
36
|
* Structure:
|
|
29
|
-
* - 1 common watcher: config, refs/**, info/exclude, etc.
|
|
37
|
+
* - 1 common watcher: config, refs/**, info/exclude, etc. Delivers to linked sessions only; a
|
|
38
|
+
* session rooted at the common dir hears the same files from its own combined root watcher.
|
|
30
39
|
* - N root watchers: one per worktree (index, HEAD, *_HEAD, etc.)
|
|
31
40
|
*
|
|
32
41
|
* For a standard (non-worktree) repo, the common and root directories
|
|
@@ -47,8 +56,10 @@ export declare class WatchGroup implements UnifiedDisposable {
|
|
|
47
56
|
addSession(repoPath: string, gitDir: GitDir, hooks: WatchGroupHooks): void;
|
|
48
57
|
/** Remove a session from this group. Disposes its root watcher. */
|
|
49
58
|
removeSession(repoPath: string): void;
|
|
59
|
+
private getWorktreeId;
|
|
50
60
|
private ensureCommonWatcher;
|
|
51
61
|
private onCommonEvent;
|
|
62
|
+
private deliverOverflow;
|
|
52
63
|
private onRootEvent;
|
|
53
64
|
}
|
|
54
65
|
//#endregion
|
|
@@ -1,13 +1,15 @@
|
|
|
1
|
+
import { some } from "../../utils/iterable.js";
|
|
1
2
|
import { isDescendant, relative } from "../../utils/path.js";
|
|
2
|
-
import { dotGitGlobCombined, dotGitGlobCommon, dotGitGlobRoot, shouldIgnoreRepoPath } from "./watcherPatterns.js";
|
|
3
|
-
import { classifyGitDirChange } from "./classifyChange.js";
|
|
3
|
+
import { dotGitGlobCombined, dotGitGlobCommon, dotGitGlobRoot, gitDirExcludes, shouldIgnoreRepoPath } from "./watcherPatterns.js";
|
|
4
|
+
import { allWatchedRepositoryChanges, classifyGitDirChange } from "./classifyChange.js";
|
|
4
5
|
//#region ../git/src/watching/watchGroup.ts
|
|
5
6
|
/**
|
|
6
7
|
* Manages filesystem watchers for a single physical `.git` directory,
|
|
7
8
|
* shared across all worktrees that reference it.
|
|
8
9
|
*
|
|
9
10
|
* Structure:
|
|
10
|
-
* - 1 common watcher: config, refs/**, info/exclude, etc.
|
|
11
|
+
* - 1 common watcher: config, refs/**, info/exclude, etc. Delivers to linked sessions only; a
|
|
12
|
+
* session rooted at the common dir hears the same files from its own combined root watcher.
|
|
11
13
|
* - N root watchers: one per worktree (index, HEAD, *_HEAD, etc.)
|
|
12
14
|
*
|
|
13
15
|
* For a standard (non-worktree) repo, the common and root directories
|
|
@@ -47,11 +49,14 @@ var WatchGroup = class {
|
|
|
47
49
|
const rootWatcher = this.provider.createWatcher(gitDir.uri.fsPath, rootPattern, (event) => {
|
|
48
50
|
const entry = this._sessions.get(repoPath);
|
|
49
51
|
if (entry != null) this.onRootEvent(event, entry);
|
|
50
|
-
});
|
|
52
|
+
}, { excludes: gitDirExcludes });
|
|
53
|
+
const entryGitDirPath = gitDir.uri.fsPath;
|
|
51
54
|
const entry = {
|
|
52
55
|
repoPath,
|
|
53
|
-
gitDirPath:
|
|
56
|
+
gitDirPath: entryGitDirPath,
|
|
54
57
|
rootWatcher,
|
|
58
|
+
coversCommon: isStandard,
|
|
59
|
+
worktreeId: isStandard ? void 0 : this.getWorktreeId(entryGitDirPath),
|
|
55
60
|
hooks
|
|
56
61
|
};
|
|
57
62
|
this._sessions.set(repoPath, entry);
|
|
@@ -63,32 +68,65 @@ var WatchGroup = class {
|
|
|
63
68
|
if (entry == null) return;
|
|
64
69
|
entry.rootWatcher.dispose();
|
|
65
70
|
this._sessions.delete(repoPath);
|
|
66
|
-
if (this.
|
|
71
|
+
if (this._commonWatcher != null && !some(this._sessions.values(), (e) => !e.coversCommon)) {
|
|
67
72
|
this._commonWatcher.dispose();
|
|
68
73
|
this._commonWatcher = void 0;
|
|
69
74
|
}
|
|
70
75
|
}
|
|
76
|
+
getWorktreeId(gitDirPath) {
|
|
77
|
+
if (!isDescendant(gitDirPath, this.commonGitDir)) return void 0;
|
|
78
|
+
const rel = relative(this.commonGitDir, gitDirPath);
|
|
79
|
+
if (!rel.startsWith("worktrees/")) return void 0;
|
|
80
|
+
const id = rel.substring(10);
|
|
81
|
+
return id.length > 0 && !id.includes("/") ? id : void 0;
|
|
82
|
+
}
|
|
71
83
|
ensureCommonWatcher() {
|
|
72
84
|
if (this._commonWatcher != null || this._disposed) return;
|
|
73
85
|
this._commonWatcher = this.provider.createWatcher(this.commonGitDir, dotGitGlobCommon, (event) => {
|
|
74
86
|
this.onCommonEvent(event);
|
|
75
|
-
});
|
|
87
|
+
}, { excludes: gitDirExcludes });
|
|
76
88
|
}
|
|
77
89
|
onCommonEvent(event) {
|
|
90
|
+
if (event.reason === "overflow") {
|
|
91
|
+
for (const entry of this._sessions.values()) {
|
|
92
|
+
if (entry.coversCommon) continue;
|
|
93
|
+
this.deliverOverflow(entry);
|
|
94
|
+
}
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
78
97
|
if (!isDescendant(event.path, this.commonGitDir)) return;
|
|
79
98
|
const relativePath = relative(this.commonGitDir, event.path);
|
|
80
99
|
if (shouldIgnoreRepoPath(relativePath)) return;
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
100
|
+
const isFetchHead = relativePath === "FETCH_HEAD";
|
|
101
|
+
const isExclude = relativePath === "info/exclude";
|
|
102
|
+
const changes = isFetchHead ? void 0 : classifyGitDirChange(relativePath);
|
|
103
|
+
if (changes == null && !isFetchHead && !isExclude) return;
|
|
104
|
+
const ref = changes != null ? getChangedRef(relativePath) : void 0;
|
|
105
|
+
let ownerId;
|
|
106
|
+
if (changes != null && relativePath.length > 10 && relativePath.startsWith("worktrees/")) {
|
|
107
|
+
const idEnd = relativePath.indexOf("/", 10);
|
|
108
|
+
if (idEnd !== -1 && relativePath.substring(idEnd + 1) === "HEAD") ownerId = relativePath.substring(10, idEnd);
|
|
84
109
|
}
|
|
85
|
-
const changes = classifyGitDirChange(relativePath);
|
|
86
110
|
for (const entry of this._sessions.values()) {
|
|
87
|
-
if (
|
|
88
|
-
if (
|
|
111
|
+
if (entry.coversCommon) continue;
|
|
112
|
+
if (isFetchHead) {
|
|
113
|
+
entry.hooks.onFetchHeadChanged?.(entry.repoPath);
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
if (changes != null && (ownerId == null || entry.worktreeId !== ownerId)) entry.hooks.onRepoChanged(entry.repoPath, changes, ref);
|
|
117
|
+
if (isExclude) entry.hooks.onIgnoresChanged?.(entry.repoPath);
|
|
89
118
|
}
|
|
90
119
|
}
|
|
120
|
+
deliverOverflow(entry) {
|
|
121
|
+
entry.hooks.onRepoChanged(entry.repoPath, [...allWatchedRepositoryChanges]);
|
|
122
|
+
entry.hooks.onFetchHeadChanged?.(entry.repoPath);
|
|
123
|
+
entry.hooks.onIgnoresChanged?.(entry.repoPath);
|
|
124
|
+
}
|
|
91
125
|
onRootEvent(event, entry) {
|
|
126
|
+
if (event.reason === "overflow") {
|
|
127
|
+
this.deliverOverflow(entry);
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
92
130
|
if (!isDescendant(event.path, entry.gitDirPath)) return;
|
|
93
131
|
const relativePath = relative(entry.gitDirPath, event.path);
|
|
94
132
|
if (shouldIgnoreRepoPath(relativePath)) return;
|
|
@@ -97,10 +135,18 @@ var WatchGroup = class {
|
|
|
97
135
|
return;
|
|
98
136
|
}
|
|
99
137
|
const changes = classifyGitDirChange(relativePath);
|
|
100
|
-
if (changes != null) entry.hooks.onRepoChanged(entry.repoPath, changes);
|
|
138
|
+
if (changes != null) entry.hooks.onRepoChanged(entry.repoPath, changes, getChangedRef(relativePath));
|
|
101
139
|
if (relativePath === "info/exclude") entry.hooks.onIgnoresChanged?.(entry.repoPath);
|
|
102
140
|
}
|
|
103
141
|
};
|
|
142
|
+
/**
|
|
143
|
+
* The ref a `.git`-relative path names: a loose ref (or ref directory) under `refs/`, or the `HEAD`/`ORIG_HEAD`
|
|
144
|
+
* pseudo-refs. Git writes a ref as `<name>.lock` and renames it over `<name>`, so the lock file names the same ref.
|
|
145
|
+
*/
|
|
146
|
+
function getChangedRef(relativePath) {
|
|
147
|
+
const ref = relativePath.endsWith(".lock") ? relativePath.substring(0, relativePath.length - 5) : relativePath;
|
|
148
|
+
return ref.startsWith("refs/") || ref === "HEAD" || ref === "ORIG_HEAD" ? ref : void 0;
|
|
149
|
+
}
|
|
104
150
|
//#endregion
|
|
105
151
|
export { WatchGroup };
|
|
106
152
|
|