@gitkraken/core-gitlens 0.10.7 → 0.11.1
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 +39 -1
- package/dist/git/cache.d.ts +34 -2
- package/dist/git/cache.js +55 -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 +24 -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 +132 -0
- package/dist/git/watching/worktreeSummary.js +429 -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 +217 -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/plus/integrations/reads/batchEtags.d.ts +5 -2
- package/dist/plus/integrations/reads/batchEtags.js +31 -10
- package/dist/plus/integrations/reads/batchEtags.js.map +1 -1
- package/dist/plus/integrations/reads/pullRequestBranches.js +10 -3
- package/dist/plus/integrations/reads/pullRequestBranches.js.map +1 -1
- package/dist/plus/integrations/results.d.ts +16 -1
- package/dist/plus/integrations/results.js +22 -3
- package/dist/plus/integrations/results.js.map +1 -1
- package/dist/utils/event.d.ts +1 -1
- package/docs/integrations.md +5 -0
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"watchGroup.js","names":[],"sources":["../../../../git/src/watching/watchGroup.ts"],"sourcesContent":["import type { UnifiedDisposable } from '@gitlens/utils/disposable.js';\nimport { isDescendant, relative } from '@gitlens/utils/path.js';\nimport type { GitDir, RepositoryChange } from '../models/repository.js';\nimport { classifyGitDirChange } from './classifyChange.js';\nimport type { FileWatcher, FileWatchEvent, FileWatchingProvider } from './provider.js';\nimport { dotGitGlobCombined, dotGitGlobCommon, dotGitGlobRoot, shouldIgnoreRepoPath } from './watcherPatterns.js';\n\n/**\n * Hooks that the WatchGroup fires when events are interpreted.\n * These are raw, un-debounced events — the session handles debouncing.\n */\nexport interface WatchGroupHooks {\n\treadonly onRepoChanged: (repoPath: string, changes: RepositoryChange[]) => void;\n\treadonly onFetchHeadChanged?: (repoPath: string) => void;\n\treadonly onIgnoresChanged?: (repoPath: string) => void;\n}\n\n/**\n * Tracks one session's connection to a WatchGroup.\n * Each session has its own root watcher but shares the common watcher.\n */\ninterface SessionEntry {\n\treadonly repoPath: string;\n\treadonly gitDirPath: string;\n\treadonly rootWatcher: FileWatcher;\n\treadonly hooks: WatchGroupHooks;\n}\n\n/**\n * Manages filesystem watchers for a single physical `.git` directory,\n * shared across all worktrees that reference it.\n *\n * Structure:\n * - 1 common watcher: config, refs/**, info/exclude, etc.\n * - N root watchers: one per worktree (index, HEAD, *_HEAD, etc.)\n *\n * For a standard (non-worktree) repo, the common and root directories\n * are the same, but we still create separate watchers for clean separation.\n */\nexport class WatchGroup implements UnifiedDisposable {\n\tprivate readonly _sessions = new Map<string, SessionEntry>();\n\tprivate _commonWatcher: FileWatcher | undefined;\n\tprivate _disposed = false;\n\n\tconstructor(\n\t\treadonly commonGitDir: string,\n\t\tprivate readonly provider: FileWatchingProvider,\n\t) {}\n\n\t[Symbol.dispose](): void {\n\t\tthis.dispose();\n\t}\n\n\t/** Dispose the entire group (common watcher + all root watchers). */\n\tdispose(): void {\n\t\tif (this._disposed) return;\n\n\t\tthis._disposed = true;\n\n\t\tfor (const entry of this._sessions.values()) {\n\t\t\tentry.rootWatcher.dispose();\n\t\t}\n\t\tthis._sessions.clear();\n\n\t\tthis._commonWatcher?.dispose();\n\t\tthis._commonWatcher = undefined;\n\t}\n\n\tget sessions(): ReadonlyMap<string, SessionEntry> {\n\t\treturn this._sessions;\n\t}\n\n\t/** Add a session to this group. Creates the root watcher for the session's gitDir. */\n\taddSession(repoPath: string, gitDir: GitDir, hooks: WatchGroupHooks): void {\n\t\tif (this._disposed) return;\n\t\tif (this._sessions.has(repoPath)) return; // Already tracked\n\n\t\t// Determine the glob pattern for the root watcher\n\t\tconst isStandard = gitDir.commonUri == null;\n\t\tconst rootPattern = isStandard ? dotGitGlobCombined : dotGitGlobRoot;\n\n\t\tconst rootWatcher = this.provider.createWatcher(gitDir.uri.fsPath, rootPattern, (event: FileWatchEvent) => {\n\t\t\tconst entry = this._sessions.get(repoPath);\n\t\t\tif (entry != null) {\n\t\t\t\tthis.onRootEvent(event, entry);\n\t\t\t}\n\t\t});\n\n\t\tconst entry: SessionEntry = {\n\t\t\trepoPath: repoPath,\n\t\t\tgitDirPath: gitDir.uri.fsPath,\n\t\t\trootWatcher: rootWatcher,\n\t\t\thooks: hooks,\n\t\t};\n\n\t\tthis._sessions.set(repoPath, entry);\n\n\t\t// Start the common watcher if this is a worktree repo and we haven't yet.\n\t\t// Standard repos don't need a separate common watcher because their root\n\t\t// watcher uses dotGitGlobCombined which already covers all patterns.\n\t\tif (!isStandard) {\n\t\t\tthis.ensureCommonWatcher();\n\t\t}\n\t}\n\n\t/** Remove a session from this group. Disposes its root watcher. */\n\tremoveSession(repoPath: string): void {\n\t\tconst entry = this._sessions.get(repoPath);\n\t\tif (entry == null) return;\n\n\t\tentry.rootWatcher.dispose();\n\t\tthis._sessions.delete(repoPath);\n\n\t\t// If no more sessions, dispose the common watcher\n\t\tif (this._sessions.size === 0 && this._commonWatcher != null) {\n\t\t\tthis._commonWatcher.dispose();\n\t\t\tthis._commonWatcher = undefined;\n\t\t}\n\t}\n\n\tprivate ensureCommonWatcher(): void {\n\t\tif (this._commonWatcher != null || this._disposed) return;\n\n\t\tthis._commonWatcher = this.provider.createWatcher(\n\t\t\tthis.commonGitDir,\n\t\t\tdotGitGlobCommon,\n\t\t\t(event: FileWatchEvent) => {\n\t\t\t\tthis.onCommonEvent(event);\n\t\t\t},\n\t\t);\n\t}\n\n\tprivate onCommonEvent(event: FileWatchEvent): void {\n\t\t// Compute relative path from common git dir\n\t\tif (!isDescendant(event.path, this.commonGitDir)) return;\n\n\t\tconst relativePath = relative(this.commonGitDir, event.path);\n\t\tif (shouldIgnoreRepoPath(relativePath)) return;\n\n\t\t// FETCH_HEAD lives in the main .git dir, not in worktree subdirs.\n\t\t// Dispatch to all sessions so each worktree can update its last-fetched state.\n\t\tif (relativePath === 'FETCH_HEAD') {\n\t\t\tfor (const entry of this._sessions.values()) {\n\t\t\t\tentry.hooks.onFetchHeadChanged?.(entry.repoPath);\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\n\t\tconst changes = classifyGitDirChange(relativePath);\n\n\t\t// Dispatch to ALL sessions in the group\n\t\tfor (const entry of this._sessions.values()) {\n\t\t\tif (changes != null) {\n\t\t\t\tentry.hooks.onRepoChanged(entry.repoPath, changes);\n\t\t\t}\n\n\t\t\t// Check for special cases\n\t\t\tif (relativePath === 'info/exclude') {\n\t\t\t\tentry.hooks.onIgnoresChanged?.(entry.repoPath);\n\t\t\t}\n\t\t}\n\t}\n\n\tprivate onRootEvent(event: FileWatchEvent, entry: SessionEntry): void {\n\t\tif (!isDescendant(event.path, entry.gitDirPath)) return;\n\n\t\tconst relativePath = relative(entry.gitDirPath, event.path);\n\t\tif (shouldIgnoreRepoPath(relativePath)) return;\n\n\t\t// Check for FETCH_HEAD before interpretation\n\t\tif (relativePath === 'FETCH_HEAD') {\n\t\t\tentry.hooks.onFetchHeadChanged?.(entry.repoPath);\n\t\t\treturn;\n\t\t}\n\n\t\tconst changes = classifyGitDirChange(relativePath);\n\t\tif (changes != null) {\n\t\t\tentry.hooks.onRepoChanged(entry.repoPath, changes);\n\t\t}\n\n\t\tif (relativePath === 'info/exclude') {\n\t\t\tentry.hooks.onIgnoresChanged?.(entry.repoPath);\n\t\t}\n\t}\n}\n"],"mappings":";;;;;;;;;;;;;;;AAuCA,IAAa,aAAb,MAAqD;CAM1C;CACQ;CANlB,4BAA6B,IAAI,IAA0B;CAC3D;CACA,YAAoB;CAEpB,YACC,cACA,UACC;EAFQ,KAAA,eAAA;EACQ,KAAA,WAAA;CACf;CAEH,CAAC,OAAO,WAAiB;EACxB,KAAK,QAAQ;CACd;;CAGA,UAAgB;EACf,IAAI,KAAK,WAAW;EAEpB,KAAK,YAAY;EAEjB,KAAK,MAAM,SAAS,KAAK,UAAU,OAAO,GACzC,MAAM,YAAY,QAAQ;EAE3B,KAAK,UAAU,MAAM;EAErB,KAAK,gBAAgB,QAAQ;EAC7B,KAAK,iBAAiB,KAAA;CACvB;CAEA,IAAI,WAA8C;EACjD,OAAO,KAAK;CACb;;CAGA,WAAW,UAAkB,QAAgB,OAA8B;EAC1E,IAAI,KAAK,WAAW;EACpB,IAAI,KAAK,UAAU,IAAI,QAAQ,GAAG;EAGlC,MAAM,aAAa,OAAO,aAAa;EACvC,MAAM,cAAc,aAAa,qBAAqB;EAEtD,MAAM,cAAc,KAAK,SAAS,cAAc,OAAO,IAAI,QAAQ,cAAc,UAA0B;GAC1G,MAAM,QAAQ,KAAK,UAAU,IAAI,QAAQ;GACzC,IAAI,SAAS,MACZ,KAAK,YAAY,OAAO,KAAK;EAE/B,CAAC;EAED,MAAM,QAAsB;GACjB;GACV,YAAY,OAAO,IAAI;GACV;GACN;EACR;EAEA,KAAK,UAAU,IAAI,UAAU,KAAK;EAKlC,IAAI,CAAC,YACJ,KAAK,oBAAoB;CAE3B;;CAGA,cAAc,UAAwB;EACrC,MAAM,QAAQ,KAAK,UAAU,IAAI,QAAQ;EACzC,IAAI,SAAS,MAAM;EAEnB,MAAM,YAAY,QAAQ;EAC1B,KAAK,UAAU,OAAO,QAAQ;EAG9B,IAAI,KAAK,UAAU,SAAS,KAAK,KAAK,kBAAkB,MAAM;GAC7D,KAAK,eAAe,QAAQ;GAC5B,KAAK,iBAAiB,KAAA;EACvB;CACD;CAEA,sBAAoC;EACnC,IAAI,KAAK,kBAAkB,QAAQ,KAAK,WAAW;EAEnD,KAAK,iBAAiB,KAAK,SAAS,cACnC,KAAK,cACL,mBACC,UAA0B;GAC1B,KAAK,cAAc,KAAK;EACzB,CACD;CACD;CAEA,cAAsB,OAA6B;EAElD,IAAI,CAAC,aAAa,MAAM,MAAM,KAAK,YAAY,GAAG;EAElD,MAAM,eAAe,SAAS,KAAK,cAAc,MAAM,IAAI;EAC3D,IAAI,qBAAqB,YAAY,GAAG;EAIxC,IAAI,iBAAiB,cAAc;GAClC,KAAK,MAAM,SAAS,KAAK,UAAU,OAAO,GACzC,MAAM,MAAM,qBAAqB,MAAM,QAAQ;GAEhD;EACD;EAEA,MAAM,UAAU,qBAAqB,YAAY;EAGjD,KAAK,MAAM,SAAS,KAAK,UAAU,OAAO,GAAG;GAC5C,IAAI,WAAW,MACd,MAAM,MAAM,cAAc,MAAM,UAAU,OAAO;GAIlD,IAAI,iBAAiB,gBACpB,MAAM,MAAM,mBAAmB,MAAM,QAAQ;EAE/C;CACD;CAEA,YAAoB,OAAuB,OAA2B;EACrE,IAAI,CAAC,aAAa,MAAM,MAAM,MAAM,UAAU,GAAG;EAEjD,MAAM,eAAe,SAAS,MAAM,YAAY,MAAM,IAAI;EAC1D,IAAI,qBAAqB,YAAY,GAAG;EAGxC,IAAI,iBAAiB,cAAc;GAClC,MAAM,MAAM,qBAAqB,MAAM,QAAQ;GAC/C;EACD;EAEA,MAAM,UAAU,qBAAqB,YAAY;EACjD,IAAI,WAAW,MACd,MAAM,MAAM,cAAc,MAAM,UAAU,OAAO;EAGlD,IAAI,iBAAiB,gBACpB,MAAM,MAAM,mBAAmB,MAAM,QAAQ;CAE/C;AACD"}
|
|
1
|
+
{"version":3,"file":"watchGroup.js","names":[],"sources":["../../../../git/src/watching/watchGroup.ts"],"sourcesContent":["import type { UnifiedDisposable } from '@gitlens/utils/disposable.js';\nimport { some } from '@gitlens/utils/iterable.js';\nimport { isDescendant, relative } from '@gitlens/utils/path.js';\nimport type { GitDir, RepositoryChange } from '../models/repository.js';\nimport { allWatchedRepositoryChanges, classifyGitDirChange } from './classifyChange.js';\nimport type { FileWatcher, FileWatchEvent, FileWatchingProvider } from './provider.js';\nimport {\n\tdotGitGlobCombined,\n\tdotGitGlobCommon,\n\tdotGitGlobRoot,\n\tgitDirExcludes,\n\tshouldIgnoreRepoPath,\n} from './watcherPatterns.js';\n\n/**\n * Hooks that the WatchGroup fires when events are interpreted.\n * These are raw, un-debounced events — the session handles debouncing.\n */\nexport interface WatchGroupHooks {\n\t/**\n\t * `ref` is the full ref name (or ref directory) or pseudo-ref (`HEAD`, `ORIG_HEAD`) whose file changed, with a\n\t * trailing `.lock` stripped; `undefined` for any other `.git` file.\n\t */\n\treadonly onRepoChanged: (repoPath: string, changes: RepositoryChange[], ref?: string) => void;\n\treadonly onFetchHeadChanged?: (repoPath: string) => void;\n\treadonly onIgnoresChanged?: (repoPath: string) => void;\n}\n\n/**\n * Tracks one session's connection to a WatchGroup.\n * Each session has its own root watcher but shares the common watcher.\n */\ninterface SessionEntry {\n\treadonly repoPath: string;\n\treadonly gitDirPath: string;\n\treadonly rootWatcher: FileWatcher;\n\t/** True when the root watcher is on the common dir itself, so it already sees every common file */\n\treadonly coversCommon: boolean;\n\t/** For a linked session, its `<n>` in `<common>/worktrees/<n>`; otherwise `undefined` */\n\treadonly worktreeId: string | undefined;\n\treadonly hooks: WatchGroupHooks;\n}\n\n/**\n * Manages filesystem watchers for a single physical `.git` directory,\n * shared across all worktrees that reference it.\n *\n * Structure:\n * - 1 common watcher: config, refs/**, info/exclude, etc. Delivers to linked sessions only; a\n * session rooted at the common dir hears the same files from its own combined root watcher.\n * - N root watchers: one per worktree (index, HEAD, *_HEAD, etc.)\n *\n * For a standard (non-worktree) repo, the common and root directories\n * are the same, but we still create separate watchers for clean separation.\n */\nexport class WatchGroup implements UnifiedDisposable {\n\tprivate readonly _sessions = new Map<string, SessionEntry>();\n\tprivate _commonWatcher: FileWatcher | undefined;\n\tprivate _disposed = false;\n\n\tconstructor(\n\t\treadonly commonGitDir: string,\n\t\tprivate readonly provider: FileWatchingProvider,\n\t) {}\n\n\t[Symbol.dispose](): void {\n\t\tthis.dispose();\n\t}\n\n\t/** Dispose the entire group (common watcher + all root watchers). */\n\tdispose(): void {\n\t\tif (this._disposed) return;\n\n\t\tthis._disposed = true;\n\n\t\tfor (const entry of this._sessions.values()) {\n\t\t\tentry.rootWatcher.dispose();\n\t\t}\n\t\tthis._sessions.clear();\n\n\t\tthis._commonWatcher?.dispose();\n\t\tthis._commonWatcher = undefined;\n\t}\n\n\tget sessions(): ReadonlyMap<string, SessionEntry> {\n\t\treturn this._sessions;\n\t}\n\n\t/** Add a session to this group. Creates the root watcher for the session's gitDir. */\n\taddSession(repoPath: string, gitDir: GitDir, hooks: WatchGroupHooks): void {\n\t\tif (this._disposed) return;\n\t\tif (this._sessions.has(repoPath)) return; // Already tracked\n\n\t\t// Determine the glob pattern for the root watcher\n\t\tconst isStandard = gitDir.commonUri == null;\n\t\tconst rootPattern = isStandard ? dotGitGlobCombined : dotGitGlobRoot;\n\n\t\tconst rootWatcher = this.provider.createWatcher(\n\t\t\tgitDir.uri.fsPath,\n\t\t\trootPattern,\n\t\t\t(event: FileWatchEvent) => {\n\t\t\t\tconst entry = this._sessions.get(repoPath);\n\t\t\t\tif (entry != null) {\n\t\t\t\t\tthis.onRootEvent(event, entry);\n\t\t\t\t}\n\t\t\t},\n\t\t\t{ excludes: gitDirExcludes },\n\t\t);\n\n\t\tconst entryGitDirPath = gitDir.uri.fsPath;\n\t\tconst entry: SessionEntry = {\n\t\t\trepoPath: repoPath,\n\t\t\tgitDirPath: entryGitDirPath,\n\t\t\trootWatcher: rootWatcher,\n\t\t\tcoversCommon: isStandard,\n\t\t\tworktreeId: isStandard ? undefined : this.getWorktreeId(entryGitDirPath),\n\t\t\thooks: hooks,\n\t\t};\n\n\t\tthis._sessions.set(repoPath, entry);\n\n\t\t// Start the common watcher if this is a worktree repo and we haven't yet.\n\t\t// Standard repos don't need a separate common watcher because their root\n\t\t// watcher uses dotGitGlobCombined which already covers all patterns.\n\t\tif (!isStandard) {\n\t\t\tthis.ensureCommonWatcher();\n\t\t}\n\t}\n\n\t/** Remove a session from this group. Disposes its root watcher. */\n\tremoveSession(repoPath: string): void {\n\t\tconst entry = this._sessions.get(repoPath);\n\t\tif (entry == null) return;\n\n\t\tentry.rootWatcher.dispose();\n\t\tthis._sessions.delete(repoPath);\n\n\t\t// Once no linked session is left, dispose the common watcher — it delivers to linked sessions only\n\t\tif (this._commonWatcher != null && !some(this._sessions.values(), e => !e.coversCommon)) {\n\t\t\tthis._commonWatcher.dispose();\n\t\t\tthis._commonWatcher = undefined;\n\t\t}\n\t}\n\n\tprivate getWorktreeId(gitDirPath: string): string | undefined {\n\t\tif (!isDescendant(gitDirPath, this.commonGitDir)) return undefined;\n\n\t\tconst rel = relative(this.commonGitDir, gitDirPath);\n\t\tif (!rel.startsWith('worktrees/')) return undefined;\n\n\t\tconst id = rel.substring(10);\n\t\treturn id.length > 0 && !id.includes('/') ? id : undefined;\n\t}\n\n\tprivate ensureCommonWatcher(): void {\n\t\tif (this._commonWatcher != null || this._disposed) return;\n\n\t\tthis._commonWatcher = this.provider.createWatcher(\n\t\t\tthis.commonGitDir,\n\t\t\tdotGitGlobCommon,\n\t\t\t(event: FileWatchEvent) => {\n\t\t\t\tthis.onCommonEvent(event);\n\t\t\t},\n\t\t\t{ excludes: gitDirExcludes },\n\t\t);\n\t}\n\n\tprivate onCommonEvent(event: FileWatchEvent): void {\n\t\tif (event.reason === 'overflow') {\n\t\t\tfor (const entry of this._sessions.values()) {\n\t\t\t\tif (entry.coversCommon) continue;\n\n\t\t\t\tthis.deliverOverflow(entry);\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\n\t\t// Compute relative path from common git dir\n\t\tif (!isDescendant(event.path, this.commonGitDir)) return;\n\n\t\tconst relativePath = relative(this.commonGitDir, event.path);\n\t\tif (shouldIgnoreRepoPath(relativePath)) return;\n\n\t\tconst isFetchHead = relativePath === 'FETCH_HEAD';\n\t\tconst isExclude = relativePath === 'info/exclude';\n\t\tconst changes = isFetchHead ? undefined : classifyGitDirChange(relativePath);\n\t\tif (changes == null && !isFetchHead && !isExclude) return;\n\n\t\tconst ref = changes != null ? getChangedRef(relativePath) : undefined;\n\n\t\t// `worktrees/<n>/HEAD`: the owner hears its own HEAD from its root watcher. Its `locked` and `gitdir` aren't in\n\t\t// the root glob, so the owner hears those only from here\n\t\tlet ownerId: string | undefined;\n\t\tif (changes != null && relativePath.length > 10 && relativePath.startsWith('worktrees/')) {\n\t\t\tconst idEnd = relativePath.indexOf('/', 10);\n\t\t\tif (idEnd !== -1 && relativePath.substring(idEnd + 1) === 'HEAD') {\n\t\t\t\townerId = relativePath.substring(10, idEnd);\n\t\t\t}\n\t\t}\n\n\t\tfor (const entry of this._sessions.values()) {\n\t\t\t// A session rooted at the common dir sees every common file (hooks included) through its own\n\t\t\t// combined root watcher, so delivering here too would duplicate it\n\t\t\tif (entry.coversCommon) continue;\n\n\t\t\tif (isFetchHead) {\n\t\t\t\t// FETCH_HEAD lives in the main .git dir; each worktree updates its last-fetched state\n\t\t\t\tentry.hooks.onFetchHeadChanged?.(entry.repoPath);\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tif (changes != null && (ownerId == null || entry.worktreeId !== ownerId)) {\n\t\t\t\tentry.hooks.onRepoChanged(entry.repoPath, changes, ref);\n\t\t\t}\n\n\t\t\tif (isExclude) {\n\t\t\t\tentry.hooks.onIgnoresChanged?.(entry.repoPath);\n\t\t\t}\n\t\t}\n\t}\n\n\tprivate deliverOverflow(entry: SessionEntry): void {\n\t\t// Every kind, no ref (ref kinds stay unnamed). Never `'unknown'` — the cache would unregister the path\n\t\tentry.hooks.onRepoChanged(entry.repoPath, [...allWatchedRepositoryChanges]);\n\t\tentry.hooks.onFetchHeadChanged?.(entry.repoPath);\n\t\tentry.hooks.onIgnoresChanged?.(entry.repoPath);\n\t}\n\n\tprivate onRootEvent(event: FileWatchEvent, entry: SessionEntry): void {\n\t\tif (event.reason === 'overflow') {\n\t\t\tthis.deliverOverflow(entry);\n\t\t\treturn;\n\t\t}\n\n\t\tif (!isDescendant(event.path, entry.gitDirPath)) return;\n\n\t\tconst relativePath = relative(entry.gitDirPath, event.path);\n\t\tif (shouldIgnoreRepoPath(relativePath)) return;\n\n\t\t// Check for FETCH_HEAD before interpretation\n\t\tif (relativePath === 'FETCH_HEAD') {\n\t\t\tentry.hooks.onFetchHeadChanged?.(entry.repoPath);\n\t\t\treturn;\n\t\t}\n\n\t\tconst changes = classifyGitDirChange(relativePath);\n\t\tif (changes != null) {\n\t\t\tentry.hooks.onRepoChanged(entry.repoPath, changes, getChangedRef(relativePath));\n\t\t}\n\n\t\tif (relativePath === 'info/exclude') {\n\t\t\tentry.hooks.onIgnoresChanged?.(entry.repoPath);\n\t\t}\n\t}\n}\n\n/**\n * The ref a `.git`-relative path names: a loose ref (or ref directory) under `refs/`, or the `HEAD`/`ORIG_HEAD`\n * pseudo-refs. Git writes a ref as `<name>.lock` and renames it over `<name>`, so the lock file names the same ref.\n */\nfunction getChangedRef(relativePath: string): string | undefined {\n\tconst ref = relativePath.endsWith('.lock') ? relativePath.substring(0, relativePath.length - 5) : relativePath;\n\treturn ref.startsWith('refs/') || ref === 'HEAD' || ref === 'ORIG_HEAD' ? ref : undefined;\n}\n"],"mappings":"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"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { ResourceUsage } from "../../utils/resourceUsage.js";
|
|
2
1
|
import { UnifiedDisposable } from "../../utils/disposable.js";
|
|
3
2
|
import { Event } from "../../utils/event.js";
|
|
3
|
+
import { ResourceUsage } from "../../utils/resourceUsage.js";
|
|
4
4
|
import { WatcherRepoChangeEvent } from "./changeEvent.js";
|
|
5
5
|
import { GitIgnoreFilter } from "./gitIgnoreFilter.js";
|
|
6
6
|
import { FileWatchingProvider } from "./provider.js";
|
|
@@ -13,8 +13,17 @@ export interface WatchServiceOptions {
|
|
|
13
13
|
readonly defaultRepoDelayMs?: number;
|
|
14
14
|
/** Default debounce for working tree changes when subscriber doesn't specify. Default: 2500ms */
|
|
15
15
|
readonly defaultWorkingTreeDelayMs?: number;
|
|
16
|
+
/** Default cap on how long continuous repo activity can defer a flush. `Infinity` disables it. Default: 2000ms */
|
|
17
|
+
readonly defaultRepoMaxWaitMs?: number;
|
|
18
|
+
/** Default cap on how long continuous working tree activity can defer a flush. `Infinity` disables it. Default: 10000ms */
|
|
19
|
+
readonly defaultWorkingTreeMaxWaitMs?: number;
|
|
16
20
|
/** Returns a GitIgnoreFilter for a given repo. When absent or returns undefined, gitignore filtering is skipped. */
|
|
17
21
|
readonly getIgnoreFilter?: (repoPath: string, gitDirPath: string) => GitIgnoreFilter | undefined;
|
|
22
|
+
/**
|
|
23
|
+
* Returns the repo-relative directories an ignore rule matches, for a watching provider that prunes
|
|
24
|
+
* `excludes` natively (`supportsExcludes`). Never called for one that doesn't. A rejection keeps the static set.
|
|
25
|
+
*/
|
|
26
|
+
readonly getIgnoredDirectories?: (repoPath: string) => Promise<readonly string[]>;
|
|
18
27
|
}
|
|
19
28
|
export interface WatchHooks {
|
|
20
29
|
/** Called when FETCH_HEAD changed (for last-fetched tracking) */
|
|
@@ -47,9 +56,19 @@ export declare class RepositoryWatchService implements UnifiedDisposable {
|
|
|
47
56
|
/** Global multiplexed working-tree change event (repo path) for ALL watched repos. Fires once per
|
|
48
57
|
* debounced flush per session — the single point that drives the cache's status clock for working-tree edits. */
|
|
49
58
|
get onDidChangeWorkingTree(): Event<string>;
|
|
59
|
+
private readonly _onDidChangeFetchHead;
|
|
60
|
+
/**
|
|
61
|
+
* Global FETCH_HEAD change event (repo path) for ALL watched repos, fired as each change is seen (no debounce).
|
|
62
|
+
* FETCH_HEAD maps to no {@link RepositoryChange} on the session, so this is how a host invalidates its cached
|
|
63
|
+
* last-fetched time — the standalone service bridges it to the cache's `lastFetched`.
|
|
64
|
+
*/
|
|
65
|
+
get onDidChangeFetchHead(): Event<string>;
|
|
50
66
|
private readonly defaultRepoDelayMs?;
|
|
51
67
|
private readonly defaultWorkingTreeDelayMs?;
|
|
68
|
+
private readonly defaultRepoMaxWaitMs?;
|
|
69
|
+
private readonly defaultWorkingTreeMaxWaitMs?;
|
|
52
70
|
private readonly getIgnoreFilter?;
|
|
71
|
+
private readonly getIgnoredDirectories?;
|
|
53
72
|
private readonly provider;
|
|
54
73
|
private readonly sessionMap;
|
|
55
74
|
private readonly watchGroups;
|
|
@@ -67,6 +86,10 @@ export declare class RepositoryWatchService implements UnifiedDisposable {
|
|
|
67
86
|
*
|
|
68
87
|
* Returns a handle with the session and a dispose function.
|
|
69
88
|
* Disposing the handle decrements the ref count.
|
|
89
|
+
*
|
|
90
|
+
* `repoPath` is keyed by {@link getRepositoryKey}, and every session, event and hook carries that key — the
|
|
91
|
+
* same one the cache fences status reads on — so a host passing `C:\repo\` and one passing `c:/repo` share
|
|
92
|
+
* a session and advance the same status clock.
|
|
70
93
|
*/
|
|
71
94
|
watch(repoPath: string, gitDir: GitDir, watchHooks?: WatchHooks): WatchHandle | undefined;
|
|
72
95
|
/** Get an existing session by repo path */
|
|
@@ -83,6 +106,24 @@ export declare class RepositoryWatchService implements UnifiedDisposable {
|
|
|
83
106
|
private disconnectFromWatchGroup;
|
|
84
107
|
private startWorkingTreeWatcher;
|
|
85
108
|
private stopWorkingTreeWatcher;
|
|
109
|
+
private canPruneIgnoredDirectories;
|
|
110
|
+
/**
|
|
111
|
+
* An event the gitignore post-filter dropped. When its directory is itself ignored, the watcher is watching an
|
|
112
|
+
* ignored directory it could prune (one created after the last read), so the ignored set is read again. An ignored
|
|
113
|
+
* file in a watched directory (a root `.env`, `src/debug.log`) triggers nothing.
|
|
114
|
+
*/
|
|
115
|
+
private onIgnoredEvent;
|
|
116
|
+
/** Re-reads the git-ignored directories once ignore rules stop changing (trailing debounce, one timer per repo) */
|
|
117
|
+
private scheduleIgnoredDirectoriesRefresh;
|
|
118
|
+
private armIgnoredDirectoriesRefresh;
|
|
119
|
+
/** Single-flight: reads the git-ignored directories and, when they changed, swaps in a watcher excluding them */
|
|
120
|
+
private refreshIgnoredDirectories;
|
|
121
|
+
/**
|
|
122
|
+
* A directory the read left unexcluded is one git won't prune (or the read failed), so it never triggers a read
|
|
123
|
+
* again until ignore rules change — otherwise each of its events would spawn git.
|
|
124
|
+
*/
|
|
125
|
+
private settleUnprunedDirectories;
|
|
126
|
+
private swapWorkingTreeWatcher;
|
|
86
127
|
private refreshWorkingTreeFilter;
|
|
87
128
|
private disposeSessionRecord;
|
|
88
129
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { isDescendant, relative } from "../../utils/path.js";
|
|
2
1
|
import { Emitter } from "../../utils/event.js";
|
|
3
|
-
import {
|
|
2
|
+
import { getRepositoryKey } from "../../utils/uri.js";
|
|
3
|
+
import { isDescendant, relative } from "../../utils/path.js";
|
|
4
|
+
import { shouldIgnoreWorkingTreePath, workingTreeExcludes } from "./watcherPatterns.js";
|
|
4
5
|
import { WatchGroup } from "./watchGroup.js";
|
|
5
6
|
import { RepositoryWatchSession } from "./watchSession.js";
|
|
6
7
|
//#region ../git/src/watching/watchService.ts
|
|
@@ -10,6 +11,8 @@ import { RepositoryWatchSession } from "./watchSession.js";
|
|
|
10
11
|
* tighter bound here would mark batches incomplete when they filter down to almost nothing.
|
|
11
12
|
*/
|
|
12
13
|
const maxBufferedWorkingTreePaths = 5e3;
|
|
14
|
+
/** Trailing debounce before the git-ignored directory set is re-read after an ignore rule changes */
|
|
15
|
+
const ignoredDirectoriesRefreshDelayMs = 500;
|
|
13
16
|
/**
|
|
14
17
|
* Global service that multiplexes repository watching across all repos.
|
|
15
18
|
*
|
|
@@ -33,9 +36,21 @@ var RepositoryWatchService = class {
|
|
|
33
36
|
get onDidChangeWorkingTree() {
|
|
34
37
|
return this._onDidChangeWorkingTree.event;
|
|
35
38
|
}
|
|
39
|
+
_onDidChangeFetchHead = new Emitter();
|
|
40
|
+
/**
|
|
41
|
+
* Global FETCH_HEAD change event (repo path) for ALL watched repos, fired as each change is seen (no debounce).
|
|
42
|
+
* FETCH_HEAD maps to no {@link RepositoryChange} on the session, so this is how a host invalidates its cached
|
|
43
|
+
* last-fetched time — the standalone service bridges it to the cache's `lastFetched`.
|
|
44
|
+
*/
|
|
45
|
+
get onDidChangeFetchHead() {
|
|
46
|
+
return this._onDidChangeFetchHead.event;
|
|
47
|
+
}
|
|
36
48
|
defaultRepoDelayMs;
|
|
37
49
|
defaultWorkingTreeDelayMs;
|
|
50
|
+
defaultRepoMaxWaitMs;
|
|
51
|
+
defaultWorkingTreeMaxWaitMs;
|
|
38
52
|
getIgnoreFilter;
|
|
53
|
+
getIgnoredDirectories;
|
|
39
54
|
provider;
|
|
40
55
|
sessionMap = /* @__PURE__ */ new Map();
|
|
41
56
|
watchGroups = /* @__PURE__ */ new Map();
|
|
@@ -47,7 +62,10 @@ var RepositoryWatchService = class {
|
|
|
47
62
|
this.provider = options.watchingProvider;
|
|
48
63
|
this.defaultRepoDelayMs = options.defaultRepoDelayMs;
|
|
49
64
|
this.defaultWorkingTreeDelayMs = options.defaultWorkingTreeDelayMs;
|
|
65
|
+
this.defaultRepoMaxWaitMs = options.defaultRepoMaxWaitMs;
|
|
66
|
+
this.defaultWorkingTreeMaxWaitMs = options.defaultWorkingTreeMaxWaitMs;
|
|
50
67
|
this.getIgnoreFilter = options.getIgnoreFilter;
|
|
68
|
+
this.getIgnoredDirectories = options.getIgnoredDirectories;
|
|
51
69
|
}
|
|
52
70
|
[Symbol.dispose]() {
|
|
53
71
|
this.dispose();
|
|
@@ -59,10 +77,10 @@ var RepositoryWatchService = class {
|
|
|
59
77
|
this.sessionMap.clear();
|
|
60
78
|
for (const group of this.watchGroups.values()) group.dispose();
|
|
61
79
|
this.watchGroups.clear();
|
|
62
|
-
for (const
|
|
63
|
-
this.wtWatchers.clear();
|
|
80
|
+
for (const repoPath of [...this.wtWatchers.keys()]) this.stopWorkingTreeWatcher(repoPath);
|
|
64
81
|
this._onDidChangeRepository.dispose();
|
|
65
82
|
this._onDidChangeWorkingTree.dispose();
|
|
83
|
+
this._onDidChangeFetchHead.dispose();
|
|
66
84
|
}
|
|
67
85
|
/**
|
|
68
86
|
* Start watching a repository. Reference-counted:
|
|
@@ -71,9 +89,14 @@ var RepositoryWatchService = class {
|
|
|
71
89
|
*
|
|
72
90
|
* Returns a handle with the session and a dispose function.
|
|
73
91
|
* Disposing the handle decrements the ref count.
|
|
92
|
+
*
|
|
93
|
+
* `repoPath` is keyed by {@link getRepositoryKey}, and every session, event and hook carries that key — the
|
|
94
|
+
* same one the cache fences status reads on — so a host passing `C:\repo\` and one passing `c:/repo` share
|
|
95
|
+
* a session and advance the same status clock.
|
|
74
96
|
*/
|
|
75
97
|
watch(repoPath, gitDir, watchHooks) {
|
|
76
98
|
if (this._disposed) return void 0;
|
|
99
|
+
repoPath = getRepositoryKey(repoPath);
|
|
77
100
|
let record = this.sessionMap.get(repoPath);
|
|
78
101
|
if (record != null) {
|
|
79
102
|
record.refCount++;
|
|
@@ -83,6 +106,8 @@ var RepositoryWatchService = class {
|
|
|
83
106
|
repoPath,
|
|
84
107
|
defaultRepoDelayMs: this.defaultRepoDelayMs,
|
|
85
108
|
defaultWorkingTreeDelayMs: this.defaultWorkingTreeDelayMs,
|
|
109
|
+
defaultRepoMaxWaitMs: this.defaultRepoMaxWaitMs,
|
|
110
|
+
defaultWorkingTreeMaxWaitMs: this.defaultWorkingTreeMaxWaitMs,
|
|
86
111
|
suspended: this._suspended,
|
|
87
112
|
lifecycle: {
|
|
88
113
|
onFirstRepoSubscriber: () => {
|
|
@@ -138,7 +163,7 @@ var RepositoryWatchService = class {
|
|
|
138
163
|
}
|
|
139
164
|
/** Get an existing session by repo path */
|
|
140
165
|
getSession(repoPath) {
|
|
141
|
-
return this.sessionMap.get(repoPath)?.session;
|
|
166
|
+
return this.sessionMap.get(getRepositoryKey(repoPath))?.session;
|
|
142
167
|
}
|
|
143
168
|
/** Resource usage retained by repository watchers. */
|
|
144
169
|
getResourceUsage() {
|
|
@@ -181,10 +206,11 @@ var RepositoryWatchService = class {
|
|
|
181
206
|
const { session, gitDir, watchHooks } = record;
|
|
182
207
|
const commonGitDir = (gitDir.commonUri ?? gitDir.uri).fsPath;
|
|
183
208
|
this.getOrCreateWatchGroup(commonGitDir).addSession(session.repoPath, gitDir, {
|
|
184
|
-
onRepoChanged: function(_repoPath, changes) {
|
|
185
|
-
session.pushRepoChanges(changes);
|
|
209
|
+
onRepoChanged: function(_repoPath, changes, ref) {
|
|
210
|
+
session.pushRepoChanges(changes, ref);
|
|
186
211
|
},
|
|
187
|
-
onFetchHeadChanged:
|
|
212
|
+
onFetchHeadChanged: (repoPath) => {
|
|
213
|
+
this._onDidChangeFetchHead.fire(repoPath);
|
|
188
214
|
for (const h of watchHooks) h.onFetchHeadChanged?.(repoPath);
|
|
189
215
|
},
|
|
190
216
|
onIgnoresChanged: (repoPath) => {
|
|
@@ -205,6 +231,8 @@ var RepositoryWatchService = class {
|
|
|
205
231
|
const { session, gitDir, watchHooks } = record;
|
|
206
232
|
const repoPath = session.repoPath;
|
|
207
233
|
if (this.wtWatchers.has(repoPath)) return;
|
|
234
|
+
const canPrune = this.canPruneIgnoredDirectories();
|
|
235
|
+
let entry;
|
|
208
236
|
let filter;
|
|
209
237
|
if (this.getIgnoreFilter != null) filter = this.getIgnoreFilter(repoPath, gitDir.uri.fsPath);
|
|
210
238
|
let filterReady = filter == null;
|
|
@@ -222,15 +250,25 @@ var RepositoryWatchService = class {
|
|
|
222
250
|
filterReady = true;
|
|
223
251
|
buffered = void 0;
|
|
224
252
|
});
|
|
225
|
-
const
|
|
253
|
+
const onIgnoreRulesChanged = () => {
|
|
254
|
+
if (filter != null) filter.refresh();
|
|
255
|
+
if (entry != null) this.scheduleIgnoredDirectoriesRefresh(entry);
|
|
256
|
+
for (const h of watchHooks) h.onGitIgnoreChanged?.(repoPath);
|
|
257
|
+
session.pushRepoChanges(["ignores"]);
|
|
258
|
+
};
|
|
259
|
+
const onEvent = (event) => {
|
|
260
|
+
if (event.reason === "overflow") {
|
|
261
|
+
session.pushWorkingTreeChanges([], true);
|
|
262
|
+
onIgnoreRulesChanged();
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
226
265
|
if (shouldIgnoreWorkingTreePath(event.path)) return;
|
|
227
266
|
const relativePath = isDescendant(event.path, repoPath) ? relative(repoPath, event.path) : void 0;
|
|
228
267
|
if (relativePath === ".gitignore") {
|
|
229
|
-
|
|
230
|
-
for (const h of watchHooks) h.onGitIgnoreChanged?.(repoPath);
|
|
231
|
-
session.pushRepoChanges(["ignores"]);
|
|
268
|
+
onIgnoreRulesChanged();
|
|
232
269
|
return;
|
|
233
270
|
}
|
|
271
|
+
if (relativePath?.endsWith("/.gitignore") && !(filterReady && filter?.isIgnored(relativePath))) onIgnoreRulesChanged();
|
|
234
272
|
if (!filterReady) {
|
|
235
273
|
if (buffered != null && buffered.length >= maxBufferedWorkingTreePaths) {
|
|
236
274
|
bufferIncomplete = true;
|
|
@@ -239,25 +277,139 @@ var RepositoryWatchService = class {
|
|
|
239
277
|
buffered?.push(event.path);
|
|
240
278
|
return;
|
|
241
279
|
}
|
|
242
|
-
if (filter != null) {
|
|
243
|
-
if (
|
|
280
|
+
if (filter != null && relativePath != null && filter.isIgnored(relativePath)) {
|
|
281
|
+
if (canPrune && entry != null) this.onIgnoredEvent(entry, filter, relativePath);
|
|
282
|
+
return;
|
|
244
283
|
}
|
|
245
284
|
session.pushWorkingTreeChanges([event.path]);
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
285
|
+
};
|
|
286
|
+
entry = {
|
|
287
|
+
session,
|
|
288
|
+
onEvent,
|
|
289
|
+
filter,
|
|
290
|
+
watcher: this.provider.createWatcher(repoPath, "**", onEvent, { excludes: workingTreeExcludes }),
|
|
291
|
+
ignoredDirectories: [],
|
|
292
|
+
generation: 0,
|
|
293
|
+
reading: false,
|
|
294
|
+
rereadQueued: false
|
|
295
|
+
};
|
|
296
|
+
this.wtWatchers.set(repoPath, entry);
|
|
297
|
+
if (canPrune) this.refreshIgnoredDirectories(entry, repoPath);
|
|
251
298
|
}
|
|
252
299
|
stopWorkingTreeWatcher(repoPath) {
|
|
253
300
|
const entry = this.wtWatchers.get(repoPath);
|
|
254
301
|
if (entry == null) return;
|
|
302
|
+
entry.generation++;
|
|
303
|
+
clearTimeout(entry.refreshTimer);
|
|
304
|
+
entry.refreshTimer = void 0;
|
|
255
305
|
entry.watcher.dispose();
|
|
256
306
|
this.wtWatchers.delete(repoPath);
|
|
257
307
|
}
|
|
308
|
+
canPruneIgnoredDirectories() {
|
|
309
|
+
return this.provider.supportsExcludes === true && this.getIgnoredDirectories != null;
|
|
310
|
+
}
|
|
311
|
+
/**
|
|
312
|
+
* An event the gitignore post-filter dropped. When its directory is itself ignored, the watcher is watching an
|
|
313
|
+
* ignored directory it could prune (one created after the last read), so the ignored set is read again. An ignored
|
|
314
|
+
* file in a watched directory (a root `.env`, `src/debug.log`) triggers nothing.
|
|
315
|
+
*/
|
|
316
|
+
onIgnoredEvent(entry, filter, relativePath) {
|
|
317
|
+
if (entry.refreshTimer != null || entry.rereadQueued) return;
|
|
318
|
+
let ignoredDir;
|
|
319
|
+
for (let slash = relativePath.indexOf("/"); slash !== -1; slash = relativePath.indexOf("/", slash + 1)) {
|
|
320
|
+
const dir = relativePath.slice(0, slash);
|
|
321
|
+
if (entry.unpruned?.has(dir) || entry.unprunable?.has(dir)) return;
|
|
322
|
+
if (filter.isIgnored(`${dir}/`)) {
|
|
323
|
+
ignoredDir = dir;
|
|
324
|
+
break;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
if (ignoredDir == null) return;
|
|
328
|
+
entry.unpruned ??= /* @__PURE__ */ new Set();
|
|
329
|
+
entry.unpruned.add(ignoredDir);
|
|
330
|
+
if (entry.reading) {
|
|
331
|
+
entry.rereadQueued = true;
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
this.armIgnoredDirectoriesRefresh(entry);
|
|
335
|
+
}
|
|
336
|
+
/** Re-reads the git-ignored directories once ignore rules stop changing (trailing debounce, one timer per repo) */
|
|
337
|
+
scheduleIgnoredDirectoriesRefresh(entry) {
|
|
338
|
+
if (!this.canPruneIgnoredDirectories()) return;
|
|
339
|
+
entry.generation++;
|
|
340
|
+
entry.unprunable = void 0;
|
|
341
|
+
clearTimeout(entry.refreshTimer);
|
|
342
|
+
this.armIgnoredDirectoriesRefresh(entry);
|
|
343
|
+
}
|
|
344
|
+
armIgnoredDirectoriesRefresh(entry) {
|
|
345
|
+
entry.refreshTimer = setTimeout(() => {
|
|
346
|
+
entry.refreshTimer = void 0;
|
|
347
|
+
const repoPath = entry.session.repoPath;
|
|
348
|
+
if (this.wtWatchers.get(repoPath) !== entry) return;
|
|
349
|
+
if (entry.reading) {
|
|
350
|
+
entry.rereadQueued = true;
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
this.refreshIgnoredDirectories(entry, repoPath);
|
|
354
|
+
}, ignoredDirectoriesRefreshDelayMs);
|
|
355
|
+
}
|
|
356
|
+
/** Single-flight: reads the git-ignored directories and, when they changed, swaps in a watcher excluding them */
|
|
357
|
+
async refreshIgnoredDirectories(entry, repoPath) {
|
|
358
|
+
if (this.getIgnoredDirectories == null) return;
|
|
359
|
+
entry.reading = true;
|
|
360
|
+
try {
|
|
361
|
+
const generation = entry.generation;
|
|
362
|
+
const unpruned = entry.unpruned?.size ? [...entry.unpruned] : void 0;
|
|
363
|
+
let directories;
|
|
364
|
+
try {
|
|
365
|
+
directories = await this.getIgnoredDirectories(repoPath);
|
|
366
|
+
} catch {
|
|
367
|
+
directories = void 0;
|
|
368
|
+
}
|
|
369
|
+
if (entry.generation === generation) {
|
|
370
|
+
if (directories != null) this.swapWorkingTreeWatcher(entry, repoPath, directories);
|
|
371
|
+
if (unpruned != null) this.settleUnprunedDirectories(entry, unpruned, directories);
|
|
372
|
+
}
|
|
373
|
+
} finally {
|
|
374
|
+
entry.reading = false;
|
|
375
|
+
}
|
|
376
|
+
if (entry.rereadQueued) {
|
|
377
|
+
entry.rereadQueued = false;
|
|
378
|
+
if (this.wtWatchers.get(repoPath) === entry) this.refreshIgnoredDirectories(entry, repoPath);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
/**
|
|
382
|
+
* A directory the read left unexcluded is one git won't prune (or the read failed), so it never triggers a read
|
|
383
|
+
* again until ignore rules change — otherwise each of its events would spawn git.
|
|
384
|
+
*/
|
|
385
|
+
settleUnprunedDirectories(entry, unpruned, directories) {
|
|
386
|
+
for (const dir of unpruned) {
|
|
387
|
+
entry.unpruned?.delete(dir);
|
|
388
|
+
if (directories?.some((d) => d === dir || dir.startsWith(`${d}/`))) continue;
|
|
389
|
+
entry.unprunable ??= /* @__PURE__ */ new Set();
|
|
390
|
+
entry.unprunable.add(dir);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
swapWorkingTreeWatcher(entry, repoPath, directories) {
|
|
394
|
+
const previous = entry.ignoredDirectories;
|
|
395
|
+
if (previous.length === directories.length && previous.every((d) => directories.includes(d))) return;
|
|
396
|
+
let watcher;
|
|
397
|
+
try {
|
|
398
|
+
watcher = this.provider.createWatcher(repoPath, "**", entry.onEvent, { excludes: [...workingTreeExcludes, ...directories] });
|
|
399
|
+
} catch {
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
const previousWatcher = entry.watcher;
|
|
403
|
+
entry.watcher = watcher;
|
|
404
|
+
entry.ignoredDirectories = directories;
|
|
405
|
+
previousWatcher.dispose();
|
|
406
|
+
if (previous.some((d) => !directories.some((n) => n === d || d.startsWith(`${n}/`)))) entry.session.pushWorkingTreeChanges([], true);
|
|
407
|
+
}
|
|
258
408
|
refreshWorkingTreeFilter(repoPath) {
|
|
259
409
|
const entry = this.wtWatchers.get(repoPath);
|
|
260
|
-
if (entry
|
|
410
|
+
if (entry == null) return;
|
|
411
|
+
if (entry.filter != null) entry.filter.refresh();
|
|
412
|
+
this.scheduleIgnoredDirectoriesRefresh(entry);
|
|
261
413
|
}
|
|
262
414
|
disposeSessionRecord(record) {
|
|
263
415
|
this.disconnectFromWatchGroup(record);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"watchService.js","names":[],"sources":["../../../../git/src/watching/watchService.ts"],"sourcesContent":["import type { UnifiedDisposable } from '@gitlens/utils/disposable.js';\nimport type { Event } from '@gitlens/utils/event.js';\nimport { Emitter } from '@gitlens/utils/event.js';\nimport { isDescendant, relative } from '@gitlens/utils/path.js';\nimport type { ResourceUsage } from '@gitlens/utils/resourceUsage.js';\nimport type { GitDir, RepositoryChange } from '../models/repository.js';\nimport type { WatcherRepoChangeEvent } from './changeEvent.js';\nimport type { GitIgnoreFilter } from './gitIgnoreFilter.js';\nimport type { FileWatcher, FileWatchingProvider } from './provider.js';\nimport { shouldIgnoreWorkingTreePath } from './watcherPatterns.js';\nimport type { WatchGroupHooks } from './watchGroup.js';\nimport { WatchGroup } from './watchGroup.js';\nimport { RepositoryWatchSession } from './watchSession.js';\n\n/**\n * Cap on paths held while the gitignore filter loads. Higher than the session's own cap because these are\n * pre-filter — most of a build's output is typically gitignored and would be discarded on replay, so a\n * tighter bound here would mark batches incomplete when they filter down to almost nothing.\n */\nconst maxBufferedWorkingTreePaths = 5000;\n\nexport interface WatchServiceOptions {\n\treadonly watchingProvider: FileWatchingProvider;\n\t/** Default debounce for repo changes when subscriber doesn't specify. Default: 250ms */\n\treadonly defaultRepoDelayMs?: number;\n\t/** Default debounce for working tree changes when subscriber doesn't specify. Default: 2500ms */\n\treadonly defaultWorkingTreeDelayMs?: number;\n\t/** Returns a GitIgnoreFilter for a given repo. When absent or returns undefined, gitignore filtering is skipped. */\n\treadonly getIgnoreFilter?: (repoPath: string, gitDirPath: string) => GitIgnoreFilter | undefined;\n}\n\nexport interface WatchHooks {\n\t/** Called when FETCH_HEAD changed (for last-fetched tracking) */\n\treadonly onFetchHeadChanged?: (repoPath: string) => void;\n\t/** Called when .git/info/exclude changed */\n\treadonly onIgnoresChanged?: (repoPath: string) => void;\n\t/** Called when .gitignore changed in the working tree (extension watches this separately) */\n\treadonly onGitIgnoreChanged?: (repoPath: string) => void;\n}\n\nexport interface WatchHandle {\n\treadonly session: RepositoryWatchSession;\n\tdispose(): void;\n}\n\ninterface SessionRecord {\n\treadonly session: RepositoryWatchSession;\n\treadonly gitDir: GitDir;\n\t/**\n\t * Hooks contributed by all current `watch()` callers for this repo path. Earlier\n\t * implementations stored just the first caller's hooks (`WatchHooks | undefined`) and\n\t * silently dropped subsequent callers' hooks — that broke the case where a low-level\n\t * subscriber (e.g. the Graph's secondary-worktree WIP watcher) opened the session\n\t * first without hooks, and a later `Repository.watch()` for the same path lost\n\t * its `markFetched` / `onIgnoresChanged` wiring. Each `watch()` call adds its hooks\n\t * to this set and removes them on dispose; fire sites iterate the set.\n\t */\n\treadonly watchHooks: Set<WatchHooks>;\n\trefCount: number;\n}\n\n/**\n * Global service that multiplexes repository watching across all repos.\n *\n * Manages:\n * - Session map: one `RepositoryWatchSession` per repo path (ref-counted)\n * - Watch groups: shared `.git` watchers across worktrees\n * - Working tree watchers: per-repo filesystem watchers with gitignore filtering\n *\n * Sessions and watch groups are created lazily on first `subscribe()`\n * within a session, and destroyed when the last subscriber leaves.\n */\nexport class RepositoryWatchService implements UnifiedDisposable {\n\tprivate readonly _onDidChangeRepository = new Emitter<WatcherRepoChangeEvent>();\n\t/** Global multiplexed event for ALL watched repos */\n\tget onDidChangeRepository(): Event<WatcherRepoChangeEvent> {\n\t\treturn this._onDidChangeRepository.event;\n\t}\n\n\tprivate readonly _onDidChangeWorkingTree = new Emitter<string>();\n\t/** Global multiplexed working-tree change event (repo path) for ALL watched repos. Fires once per\n\t * debounced flush per session — the single point that drives the cache's status clock for working-tree edits. */\n\tget onDidChangeWorkingTree(): Event<string> {\n\t\treturn this._onDidChangeWorkingTree.event;\n\t}\n\n\tprivate readonly defaultRepoDelayMs?: number;\n\tprivate readonly defaultWorkingTreeDelayMs?: number;\n\tprivate readonly getIgnoreFilter?: (repoPath: string, gitDirPath: string) => GitIgnoreFilter | undefined;\n\tprivate readonly provider: FileWatchingProvider;\n\n\tprivate readonly sessionMap = new Map<string, SessionRecord>();\n\tprivate readonly watchGroups = new Map<string, WatchGroup>();\n\tprivate readonly wtWatchers = new Map<string, { watcher: FileWatcher; filter?: GitIgnoreFilter }>();\n\tprivate _disposed = false;\n\t/** Global suspend state (driven by window focus); inherited by sessions created via {@link watch}. */\n\tprivate _suspended = false;\n\n\tconstructor(options: WatchServiceOptions) {\n\t\tthis.provider = options.watchingProvider;\n\t\tthis.defaultRepoDelayMs = options.defaultRepoDelayMs;\n\t\tthis.defaultWorkingTreeDelayMs = options.defaultWorkingTreeDelayMs;\n\t\tthis.getIgnoreFilter = options.getIgnoreFilter;\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 record of this.sessionMap.values()) {\n\t\t\tthis.disposeSessionRecord(record);\n\t\t}\n\t\tthis.sessionMap.clear();\n\n\t\tfor (const group of this.watchGroups.values()) {\n\t\t\tgroup.dispose();\n\t\t}\n\t\tthis.watchGroups.clear();\n\n\t\tfor (const entry of this.wtWatchers.values()) {\n\t\t\tentry.watcher.dispose();\n\t\t}\n\t\tthis.wtWatchers.clear();\n\n\t\tthis._onDidChangeRepository.dispose();\n\t\tthis._onDidChangeWorkingTree.dispose();\n\t}\n\n\t/**\n\t * Start watching a repository. Reference-counted:\n\t * same repoPath → same session (refCount++).\n\t * Worktrees sharing a .git dir share the WatchGroup.\n\t *\n\t * Returns a handle with the session and a dispose function.\n\t * Disposing the handle decrements the ref count.\n\t */\n\twatch(repoPath: string, gitDir: GitDir, watchHooks?: WatchHooks): WatchHandle | undefined {\n\t\tif (this._disposed) return undefined;\n\n\t\tlet record = this.sessionMap.get(repoPath);\n\t\tif (record != null) {\n\t\t\trecord.refCount++;\n\t\t\tif (watchHooks != null) {\n\t\t\t\trecord.watchHooks.add(watchHooks);\n\t\t\t}\n\t\t} else {\n\t\t\tconst session = new RepositoryWatchSession({\n\t\t\t\trepoPath: repoPath,\n\t\t\t\tdefaultRepoDelayMs: this.defaultRepoDelayMs,\n\t\t\t\tdefaultWorkingTreeDelayMs: this.defaultWorkingTreeDelayMs,\n\t\t\t\tsuspended: this._suspended,\n\t\t\t\tlifecycle: {\n\t\t\t\t\tonFirstRepoSubscriber: (): void => {\n\t\t\t\t\t\tconst rec = this.sessionMap.get(repoPath);\n\t\t\t\t\t\tif (rec != null) {\n\t\t\t\t\t\t\tthis.connectToWatchGroup(rec);\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\tonLastRepoSubscriber: (): void => {\n\t\t\t\t\t\tconst rec = this.sessionMap.get(repoPath);\n\t\t\t\t\t\tif (rec != null) {\n\t\t\t\t\t\t\tthis.disconnectFromWatchGroup(rec);\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\tonFirstWorkingTreeSubscriber: (): void => {\n\t\t\t\t\t\tconst rec = this.sessionMap.get(repoPath);\n\t\t\t\t\t\tif (rec != null) {\n\t\t\t\t\t\t\tthis.startWorkingTreeWatcher(rec);\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\tonLastWorkingTreeSubscriber: (): void => {\n\t\t\t\t\t\tthis.stopWorkingTreeWatcher(repoPath);\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tonDidFireRepoChange: (event): void => {\n\t\t\t\t\tthis._onDidChangeRepository.fire(event);\n\t\t\t\t},\n\t\t\t\tonDidFireWorkingTreeChange: (repoPath): void => {\n\t\t\t\t\tthis._onDidChangeWorkingTree.fire(repoPath);\n\t\t\t\t},\n\t\t\t});\n\n\t\t\tconst watchHooksSet = new Set<WatchHooks>();\n\t\t\tif (watchHooks != null) {\n\t\t\t\twatchHooksSet.add(watchHooks);\n\t\t\t}\n\n\t\t\trecord = {\n\t\t\t\tsession: session,\n\t\t\t\tgitDir: gitDir,\n\t\t\t\twatchHooks: watchHooksSet,\n\t\t\t\trefCount: 1,\n\t\t\t};\n\t\t\tthis.sessionMap.set(repoPath, record);\n\t\t}\n\n\t\tconst sessionRef = record.session;\n\t\tlet handleDisposed = false;\n\n\t\treturn {\n\t\t\tsession: sessionRef,\n\t\t\tdispose: (): void => {\n\t\t\t\tif (handleDisposed) return;\n\n\t\t\t\thandleDisposed = true;\n\n\t\t\t\tconst rec = this.sessionMap.get(repoPath);\n\t\t\t\tif (rec == null) return;\n\n\t\t\t\trec.refCount--;\n\t\t\t\tif (watchHooks != null) {\n\t\t\t\t\trec.watchHooks.delete(watchHooks);\n\t\t\t\t}\n\t\t\t\tif (rec.refCount <= 0) {\n\t\t\t\t\tthis.sessionMap.delete(repoPath);\n\t\t\t\t\tthis.disposeSessionRecord(rec);\n\t\t\t\t}\n\t\t\t},\n\t\t};\n\t}\n\n\t/** Get an existing session by repo path */\n\tgetSession(repoPath: string): RepositoryWatchSession | undefined {\n\t\treturn this.sessionMap.get(repoPath)?.session;\n\t}\n\n\t/** Resource usage retained by repository watchers. */\n\tgetResourceUsage(): ResourceUsage {\n\t\treturn {\n\t\t\t'watch.sessions.count': this.sessionMap.size,\n\t\t\t'watch.groups.count': this.watchGroups.size,\n\t\t\t'watch.workingTreeWatchers.count': this.wtWatchers.size,\n\t\t};\n\t}\n\n\t/** Suspend all sessions and mark the service suspended so late-joining sessions start suspended */\n\tsuspendAll(): void {\n\t\tthis._suspended = true;\n\t\tfor (const record of this.sessionMap.values()) {\n\t\t\trecord.session.suspend();\n\t\t}\n\t}\n\n\t/** Resume all sessions (clearing the global suspend state), with optional per-session delay */\n\tresumeAll(getDelay?: (session: RepositoryWatchSession) => number): void {\n\t\tthis._suspended = false;\n\t\tfor (const record of this.sessionMap.values()) {\n\t\t\tconst delayMs = getDelay?.(record.session);\n\t\t\trecord.session.resume(delayMs);\n\t\t}\n\t}\n\n\tprivate getOrCreateWatchGroup(commonGitDir: string): WatchGroup {\n\t\tlet group = this.watchGroups.get(commonGitDir);\n\t\tif (group == null) {\n\t\t\tgroup = new WatchGroup(commonGitDir, this.provider);\n\t\t\tthis.watchGroups.set(commonGitDir, group);\n\t\t}\n\t\treturn group;\n\t}\n\n\tprivate maybeDisposeWatchGroup(commonGitDir: string): void {\n\t\tconst group = this.watchGroups.get(commonGitDir);\n\t\tif (group == null) return;\n\n\t\tif (group.sessions.size === 0) {\n\t\t\tgroup.dispose();\n\t\t\tthis.watchGroups.delete(commonGitDir);\n\t\t}\n\t}\n\n\tprivate connectToWatchGroup(record: SessionRecord): void {\n\t\tconst { session, gitDir, watchHooks } = record;\n\t\tconst commonGitDir = (gitDir.commonUri ?? gitDir.uri).fsPath;\n\t\tconst group = this.getOrCreateWatchGroup(commonGitDir);\n\n\t\tconst hooks: WatchGroupHooks = {\n\t\t\tonRepoChanged: function (_repoPath: string, changes: RepositoryChange[]): void {\n\t\t\t\tsession.pushRepoChanges(changes);\n\t\t\t},\n\t\t\tonFetchHeadChanged: function (repoPath: string): void {\n\t\t\t\t// Iterate at fire time so callers that joined after the group was wired up\n\t\t\t\t// still receive events (e.g. a `Repository.watch()` whose hooks landed\n\t\t\t\t// after a hookless `GitRepositoryService.watch` registered first).\n\t\t\t\tfor (const h of watchHooks) {\n\t\t\t\t\th.onFetchHeadChanged?.(repoPath);\n\t\t\t\t}\n\t\t\t},\n\t\t\tonIgnoresChanged: (repoPath: string): void => {\n\t\t\t\tthis.refreshWorkingTreeFilter(repoPath);\n\t\t\t\tfor (const h of watchHooks) {\n\t\t\t\t\th.onIgnoresChanged?.(repoPath);\n\t\t\t\t}\n\t\t\t},\n\t\t};\n\n\t\tgroup.addSession(session.repoPath, gitDir, hooks);\n\t}\n\n\tprivate disconnectFromWatchGroup(record: SessionRecord): void {\n\t\tconst { session, gitDir } = record;\n\t\tconst commonGitDir = (gitDir.commonUri ?? gitDir.uri).fsPath;\n\t\tconst group = this.watchGroups.get(commonGitDir);\n\t\tif (group == null) return;\n\n\t\tgroup.removeSession(session.repoPath);\n\t\tthis.maybeDisposeWatchGroup(commonGitDir);\n\t}\n\n\tprivate startWorkingTreeWatcher(record: SessionRecord): void {\n\t\tconst { session, gitDir, watchHooks } = record;\n\t\tconst repoPath = session.repoPath;\n\t\tif (this.wtWatchers.has(repoPath)) return;\n\n\t\tlet filter: GitIgnoreFilter | undefined;\n\t\tif (this.getIgnoreFilter != null) {\n\t\t\tfilter = this.getIgnoreFilter(repoPath, gitDir.uri.fsPath);\n\t\t}\n\n\t\t// Buffer events while the gitignore filter loads, then replay\n\t\tlet filterReady = filter == null;\n\t\tlet buffered: string[] | undefined = filter != null ? [] : undefined;\n\t\tlet bufferIncomplete = false;\n\n\t\tif (filter != null) {\n\t\t\tvoid filter\n\t\t\t\t.ready()\n\t\t\t\t.then(() => {\n\t\t\t\t\tfilterReady = true;\n\t\t\t\t\tif (buffered?.length) {\n\t\t\t\t\t\tconst paths = filterWorkingTreePaths(buffered, repoPath, filter);\n\t\t\t\t\t\tif (paths.length > 0 || bufferIncomplete) {\n\t\t\t\t\t\t\tsession.pushWorkingTreeChanges(paths, bufferIncomplete);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tbuffered = undefined;\n\t\t\t\t\tbufferIncomplete = false;\n\t\t\t\t})\n\t\t\t\t.catch(() => {\n\t\t\t\t\tfilterReady = true;\n\t\t\t\t\tbuffered = undefined;\n\t\t\t\t});\n\t\t}\n\n\t\tconst watcher = this.provider.createWatcher(repoPath, '**', event => {\n\t\t\t// Fast noise check (node_modules, .git, .watchman-cookie-)\n\t\t\tif (shouldIgnoreWorkingTreePath(event.path)) return;\n\n\t\t\t// Detect .gitignore changes — refresh the filter and notify the host\n\t\t\tconst relativePath = isDescendant(event.path, repoPath) ? relative(repoPath, event.path) : undefined;\n\t\t\tif (relativePath === '.gitignore') {\n\t\t\t\tif (filter != null) {\n\t\t\t\t\tvoid filter.refresh();\n\t\t\t\t}\n\t\t\t\t// See `connectToWatchGroup` for the iterate-at-fire-time rationale.\n\t\t\t\tfor (const h of watchHooks) {\n\t\t\t\t\th.onGitIgnoreChanged?.(repoPath);\n\t\t\t\t}\n\t\t\t\t// Route the ignore-rule change through the repo channel as an `ignores` change (like `info/exclude`),\n\t\t\t\t// so it reaches EVERY session subscriber — including a closed secondary worktree's `watch()` `repoSub`,\n\t\t\t\t// which has no `onGitIgnoreChanged` hook and would otherwise keep a stale untracked-file list. Coalesces\n\t\t\t\t// with any hook-fired `ignores` above, so open repos don't double. (.gitignore itself is not a WT change.)\n\t\t\t\tsession.pushRepoChanges(['ignores']);\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif (!filterReady) {\n\t\t\t\t// Buffer until the gitignore filter is ready. Capped: `ready()` awaits a `core.excludesFile`\n\t\t\t\t// read, which queues behind every other git command, so this window can stay open for a long\n\t\t\t\t// time — and these paths haven't been gitignore-filtered yet, so a build's output all lands\n\t\t\t\t// here. Past the cap, stop retaining and mark the batch partial rather than growing.\n\t\t\t\tif (buffered != null && buffered.length >= maxBufferedWorkingTreePaths) {\n\t\t\t\t\tbufferIncomplete = true;\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\tbuffered?.push(event.path);\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Gitignore filter (sync after ready)\n\t\t\tif (filter != null) {\n\t\t\t\tif (relativePath != null && filter.isIgnored(relativePath)) return;\n\t\t\t}\n\n\t\t\tsession.pushWorkingTreeChanges([event.path]);\n\t\t});\n\n\t\tthis.wtWatchers.set(repoPath, { watcher: watcher, filter: filter });\n\t}\n\n\tprivate stopWorkingTreeWatcher(repoPath: string): void {\n\t\tconst entry = this.wtWatchers.get(repoPath);\n\t\tif (entry == null) return;\n\n\t\tentry.watcher.dispose();\n\t\tthis.wtWatchers.delete(repoPath);\n\t}\n\n\tprivate refreshWorkingTreeFilter(repoPath: string): void {\n\t\tconst entry = this.wtWatchers.get(repoPath);\n\t\tif (entry?.filter != null) {\n\t\t\tvoid entry.filter.refresh();\n\t\t}\n\t}\n\n\tprivate disposeSessionRecord(record: SessionRecord): void {\n\t\tthis.disconnectFromWatchGroup(record);\n\t\tthis.stopWorkingTreeWatcher(record.session.repoPath);\n\t\trecord.session.dispose();\n\t}\n}\n\nfunction filterWorkingTreePaths(absolutePaths: string[], repoPath: string, filter: GitIgnoreFilter): string[] {\n\tconst result: string[] = [];\n\tfor (const p of absolutePaths) {\n\t\tif (!isDescendant(p, repoPath)) continue;\n\n\t\tconst relativePath = relative(repoPath, p);\n\t\tif (!filter.isIgnored(relativePath)) {\n\t\t\tresult.push(p);\n\t\t}\n\t}\n\treturn result;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"watchService.js","names":[],"sources":["../../../../git/src/watching/watchService.ts"],"sourcesContent":["import type { UnifiedDisposable } from '@gitlens/utils/disposable.js';\nimport type { Event } from '@gitlens/utils/event.js';\nimport { Emitter } from '@gitlens/utils/event.js';\nimport { isDescendant, relative } from '@gitlens/utils/path.js';\nimport type { ResourceUsage } from '@gitlens/utils/resourceUsage.js';\nimport { getRepositoryKey } from '@gitlens/utils/uri.js';\nimport type { GitDir, RepositoryChange } from '../models/repository.js';\nimport type { WatcherRepoChangeEvent } from './changeEvent.js';\nimport type { GitIgnoreFilter } from './gitIgnoreFilter.js';\nimport type { FileWatcher, FileWatchEvent, FileWatchingProvider } from './provider.js';\nimport { shouldIgnoreWorkingTreePath, workingTreeExcludes } from './watcherPatterns.js';\nimport type { WatchGroupHooks } from './watchGroup.js';\nimport { WatchGroup } from './watchGroup.js';\nimport { RepositoryWatchSession } from './watchSession.js';\n\n/**\n * Cap on paths held while the gitignore filter loads. Higher than the session's own cap because these are\n * pre-filter — most of a build's output is typically gitignored and would be discarded on replay, so a\n * tighter bound here would mark batches incomplete when they filter down to almost nothing.\n */\nconst maxBufferedWorkingTreePaths = 5000;\n\n/** Trailing debounce before the git-ignored directory set is re-read after an ignore rule changes */\nconst ignoredDirectoriesRefreshDelayMs = 500;\n\ninterface WorkingTreeWatcherEntry {\n\treadonly session: RepositoryWatchSession;\n\t/** The event handler shared by every watcher this entry has had — a re-created watcher reuses it */\n\treadonly onEvent: (event: FileWatchEvent) => void;\n\treadonly filter?: GitIgnoreFilter;\n\twatcher: FileWatcher;\n\t/** The git-ignored directories the live watcher was created excluding (on top of the static set) */\n\tignoredDirectories: readonly string[];\n\t/** Bumped by every ignore-rule change and by stopping; a read that lands under a different value is stale */\n\tgeneration: number;\n\treading: boolean;\n\t/** A refresh came due while a read was in flight */\n\trereadQueued: boolean;\n\trefreshTimer?: ReturnType<typeof setTimeout>;\n\t/**\n\t * Git-ignored directories (repo-relative) whose events reached the post-filter, so the watcher isn't pruning them —\n\t * typically created after the last read (a first build's `dist/`). Each waits for a read that started after it.\n\t */\n\tunpruned?: Set<string>;\n\t/**\n\t * Directories a read showed git won't prune (they hold tracked files, or glob metacharacters in their names);\n\t * cleared when ignore rules change\n\t */\n\tunprunable?: Set<string>;\n}\n\nexport interface WatchServiceOptions {\n\treadonly watchingProvider: FileWatchingProvider;\n\t/** Default debounce for repo changes when subscriber doesn't specify. Default: 250ms */\n\treadonly defaultRepoDelayMs?: number;\n\t/** Default debounce for working tree changes when subscriber doesn't specify. Default: 2500ms */\n\treadonly defaultWorkingTreeDelayMs?: number;\n\t/** Default cap on how long continuous repo activity can defer a flush. `Infinity` disables it. Default: 2000ms */\n\treadonly defaultRepoMaxWaitMs?: number;\n\t/** Default cap on how long continuous working tree activity can defer a flush. `Infinity` disables it. Default: 10000ms */\n\treadonly defaultWorkingTreeMaxWaitMs?: number;\n\t/** Returns a GitIgnoreFilter for a given repo. When absent or returns undefined, gitignore filtering is skipped. */\n\treadonly getIgnoreFilter?: (repoPath: string, gitDirPath: string) => GitIgnoreFilter | undefined;\n\t/**\n\t * Returns the repo-relative directories an ignore rule matches, for a watching provider that prunes\n\t * `excludes` natively (`supportsExcludes`). Never called for one that doesn't. A rejection keeps the static set.\n\t */\n\treadonly getIgnoredDirectories?: (repoPath: string) => Promise<readonly string[]>;\n}\n\nexport interface WatchHooks {\n\t/** Called when FETCH_HEAD changed (for last-fetched tracking) */\n\treadonly onFetchHeadChanged?: (repoPath: string) => void;\n\t/** Called when .git/info/exclude changed */\n\treadonly onIgnoresChanged?: (repoPath: string) => void;\n\t/** Called when .gitignore changed in the working tree (extension watches this separately) */\n\treadonly onGitIgnoreChanged?: (repoPath: string) => void;\n}\n\nexport interface WatchHandle {\n\treadonly session: RepositoryWatchSession;\n\tdispose(): void;\n}\n\ninterface SessionRecord {\n\treadonly session: RepositoryWatchSession;\n\treadonly gitDir: GitDir;\n\t/**\n\t * Hooks contributed by all current `watch()` callers for this repo path. Earlier\n\t * implementations stored just the first caller's hooks (`WatchHooks | undefined`) and\n\t * silently dropped subsequent callers' hooks — that broke the case where a low-level\n\t * subscriber (e.g. the Graph's secondary-worktree WIP watcher) opened the session\n\t * first without hooks, and a later `Repository.watch()` for the same path lost\n\t * its `markFetched` / `onIgnoresChanged` wiring. Each `watch()` call adds its hooks\n\t * to this set and removes them on dispose; fire sites iterate the set.\n\t */\n\treadonly watchHooks: Set<WatchHooks>;\n\trefCount: number;\n}\n\n/**\n * Global service that multiplexes repository watching across all repos.\n *\n * Manages:\n * - Session map: one `RepositoryWatchSession` per repo path (ref-counted)\n * - Watch groups: shared `.git` watchers across worktrees\n * - Working tree watchers: per-repo filesystem watchers with gitignore filtering\n *\n * Sessions and watch groups are created lazily on first `subscribe()`\n * within a session, and destroyed when the last subscriber leaves.\n */\nexport class RepositoryWatchService implements UnifiedDisposable {\n\tprivate readonly _onDidChangeRepository = new Emitter<WatcherRepoChangeEvent>();\n\t/** Global multiplexed event for ALL watched repos */\n\tget onDidChangeRepository(): Event<WatcherRepoChangeEvent> {\n\t\treturn this._onDidChangeRepository.event;\n\t}\n\n\tprivate readonly _onDidChangeWorkingTree = new Emitter<string>();\n\t/** Global multiplexed working-tree change event (repo path) for ALL watched repos. Fires once per\n\t * debounced flush per session — the single point that drives the cache's status clock for working-tree edits. */\n\tget onDidChangeWorkingTree(): Event<string> {\n\t\treturn this._onDidChangeWorkingTree.event;\n\t}\n\n\tprivate readonly _onDidChangeFetchHead = new Emitter<string>();\n\t/**\n\t * Global FETCH_HEAD change event (repo path) for ALL watched repos, fired as each change is seen (no debounce).\n\t * FETCH_HEAD maps to no {@link RepositoryChange} on the session, so this is how a host invalidates its cached\n\t * last-fetched time — the standalone service bridges it to the cache's `lastFetched`.\n\t */\n\tget onDidChangeFetchHead(): Event<string> {\n\t\treturn this._onDidChangeFetchHead.event;\n\t}\n\n\tprivate readonly defaultRepoDelayMs?: number;\n\tprivate readonly defaultWorkingTreeDelayMs?: number;\n\tprivate readonly defaultRepoMaxWaitMs?: number;\n\tprivate readonly defaultWorkingTreeMaxWaitMs?: number;\n\tprivate readonly getIgnoreFilter?: (repoPath: string, gitDirPath: string) => GitIgnoreFilter | undefined;\n\tprivate readonly getIgnoredDirectories?: (repoPath: string) => Promise<readonly string[]>;\n\tprivate readonly provider: FileWatchingProvider;\n\n\tprivate readonly sessionMap = new Map<string, SessionRecord>();\n\tprivate readonly watchGroups = new Map<string, WatchGroup>();\n\tprivate readonly wtWatchers = new Map<string, WorkingTreeWatcherEntry>();\n\tprivate _disposed = false;\n\t/** Global suspend state (driven by window focus); inherited by sessions created via {@link watch}. */\n\tprivate _suspended = false;\n\n\tconstructor(options: WatchServiceOptions) {\n\t\tthis.provider = options.watchingProvider;\n\t\tthis.defaultRepoDelayMs = options.defaultRepoDelayMs;\n\t\tthis.defaultWorkingTreeDelayMs = options.defaultWorkingTreeDelayMs;\n\t\tthis.defaultRepoMaxWaitMs = options.defaultRepoMaxWaitMs;\n\t\tthis.defaultWorkingTreeMaxWaitMs = options.defaultWorkingTreeMaxWaitMs;\n\t\tthis.getIgnoreFilter = options.getIgnoreFilter;\n\t\tthis.getIgnoredDirectories = options.getIgnoredDirectories;\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 record of this.sessionMap.values()) {\n\t\t\tthis.disposeSessionRecord(record);\n\t\t}\n\t\tthis.sessionMap.clear();\n\n\t\tfor (const group of this.watchGroups.values()) {\n\t\t\tgroup.dispose();\n\t\t}\n\t\tthis.watchGroups.clear();\n\n\t\tfor (const repoPath of [...this.wtWatchers.keys()]) {\n\t\t\tthis.stopWorkingTreeWatcher(repoPath);\n\t\t}\n\n\t\tthis._onDidChangeRepository.dispose();\n\t\tthis._onDidChangeWorkingTree.dispose();\n\t\tthis._onDidChangeFetchHead.dispose();\n\t}\n\n\t/**\n\t * Start watching a repository. Reference-counted:\n\t * same repoPath → same session (refCount++).\n\t * Worktrees sharing a .git dir share the WatchGroup.\n\t *\n\t * Returns a handle with the session and a dispose function.\n\t * Disposing the handle decrements the ref count.\n\t *\n\t * `repoPath` is keyed by {@link getRepositoryKey}, and every session, event and hook carries that key — the\n\t * same one the cache fences status reads on — so a host passing `C:\\repo\\` and one passing `c:/repo` share\n\t * a session and advance the same status clock.\n\t */\n\twatch(repoPath: string, gitDir: GitDir, watchHooks?: WatchHooks): WatchHandle | undefined {\n\t\tif (this._disposed) return undefined;\n\n\t\trepoPath = getRepositoryKey(repoPath);\n\n\t\tlet record = this.sessionMap.get(repoPath);\n\t\tif (record != null) {\n\t\t\trecord.refCount++;\n\t\t\tif (watchHooks != null) {\n\t\t\t\trecord.watchHooks.add(watchHooks);\n\t\t\t}\n\t\t} else {\n\t\t\tconst session = new RepositoryWatchSession({\n\t\t\t\trepoPath: repoPath,\n\t\t\t\tdefaultRepoDelayMs: this.defaultRepoDelayMs,\n\t\t\t\tdefaultWorkingTreeDelayMs: this.defaultWorkingTreeDelayMs,\n\t\t\t\tdefaultRepoMaxWaitMs: this.defaultRepoMaxWaitMs,\n\t\t\t\tdefaultWorkingTreeMaxWaitMs: this.defaultWorkingTreeMaxWaitMs,\n\t\t\t\tsuspended: this._suspended,\n\t\t\t\tlifecycle: {\n\t\t\t\t\tonFirstRepoSubscriber: (): void => {\n\t\t\t\t\t\tconst rec = this.sessionMap.get(repoPath);\n\t\t\t\t\t\tif (rec != null) {\n\t\t\t\t\t\t\tthis.connectToWatchGroup(rec);\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\tonLastRepoSubscriber: (): void => {\n\t\t\t\t\t\tconst rec = this.sessionMap.get(repoPath);\n\t\t\t\t\t\tif (rec != null) {\n\t\t\t\t\t\t\tthis.disconnectFromWatchGroup(rec);\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\tonFirstWorkingTreeSubscriber: (): void => {\n\t\t\t\t\t\tconst rec = this.sessionMap.get(repoPath);\n\t\t\t\t\t\tif (rec != null) {\n\t\t\t\t\t\t\tthis.startWorkingTreeWatcher(rec);\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\tonLastWorkingTreeSubscriber: (): void => {\n\t\t\t\t\t\tthis.stopWorkingTreeWatcher(repoPath);\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tonDidFireRepoChange: (event): void => {\n\t\t\t\t\tthis._onDidChangeRepository.fire(event);\n\t\t\t\t},\n\t\t\t\tonDidFireWorkingTreeChange: (repoPath): void => {\n\t\t\t\t\tthis._onDidChangeWorkingTree.fire(repoPath);\n\t\t\t\t},\n\t\t\t});\n\n\t\t\tconst watchHooksSet = new Set<WatchHooks>();\n\t\t\tif (watchHooks != null) {\n\t\t\t\twatchHooksSet.add(watchHooks);\n\t\t\t}\n\n\t\t\trecord = {\n\t\t\t\tsession: session,\n\t\t\t\tgitDir: gitDir,\n\t\t\t\twatchHooks: watchHooksSet,\n\t\t\t\trefCount: 1,\n\t\t\t};\n\t\t\tthis.sessionMap.set(repoPath, record);\n\t\t}\n\n\t\tconst sessionRef = record.session;\n\t\tlet handleDisposed = false;\n\n\t\treturn {\n\t\t\tsession: sessionRef,\n\t\t\tdispose: (): void => {\n\t\t\t\tif (handleDisposed) return;\n\n\t\t\t\thandleDisposed = true;\n\n\t\t\t\tconst rec = this.sessionMap.get(repoPath);\n\t\t\t\tif (rec == null) return;\n\n\t\t\t\trec.refCount--;\n\t\t\t\tif (watchHooks != null) {\n\t\t\t\t\trec.watchHooks.delete(watchHooks);\n\t\t\t\t}\n\t\t\t\tif (rec.refCount <= 0) {\n\t\t\t\t\tthis.sessionMap.delete(repoPath);\n\t\t\t\t\tthis.disposeSessionRecord(rec);\n\t\t\t\t}\n\t\t\t},\n\t\t};\n\t}\n\n\t/** Get an existing session by repo path */\n\tgetSession(repoPath: string): RepositoryWatchSession | undefined {\n\t\treturn this.sessionMap.get(getRepositoryKey(repoPath))?.session;\n\t}\n\n\t/** Resource usage retained by repository watchers. */\n\tgetResourceUsage(): ResourceUsage {\n\t\treturn {\n\t\t\t'watch.sessions.count': this.sessionMap.size,\n\t\t\t'watch.groups.count': this.watchGroups.size,\n\t\t\t'watch.workingTreeWatchers.count': this.wtWatchers.size,\n\t\t};\n\t}\n\n\t/** Suspend all sessions and mark the service suspended so late-joining sessions start suspended */\n\tsuspendAll(): void {\n\t\tthis._suspended = true;\n\t\tfor (const record of this.sessionMap.values()) {\n\t\t\trecord.session.suspend();\n\t\t}\n\t}\n\n\t/** Resume all sessions (clearing the global suspend state), with optional per-session delay */\n\tresumeAll(getDelay?: (session: RepositoryWatchSession) => number): void {\n\t\tthis._suspended = false;\n\t\tfor (const record of this.sessionMap.values()) {\n\t\t\tconst delayMs = getDelay?.(record.session);\n\t\t\trecord.session.resume(delayMs);\n\t\t}\n\t}\n\n\tprivate getOrCreateWatchGroup(commonGitDir: string): WatchGroup {\n\t\tlet group = this.watchGroups.get(commonGitDir);\n\t\tif (group == null) {\n\t\t\tgroup = new WatchGroup(commonGitDir, this.provider);\n\t\t\tthis.watchGroups.set(commonGitDir, group);\n\t\t}\n\t\treturn group;\n\t}\n\n\tprivate maybeDisposeWatchGroup(commonGitDir: string): void {\n\t\tconst group = this.watchGroups.get(commonGitDir);\n\t\tif (group == null) return;\n\n\t\tif (group.sessions.size === 0) {\n\t\t\tgroup.dispose();\n\t\t\tthis.watchGroups.delete(commonGitDir);\n\t\t}\n\t}\n\n\tprivate connectToWatchGroup(record: SessionRecord): void {\n\t\tconst { session, gitDir, watchHooks } = record;\n\t\tconst commonGitDir = (gitDir.commonUri ?? gitDir.uri).fsPath;\n\t\tconst group = this.getOrCreateWatchGroup(commonGitDir);\n\n\t\tconst hooks: WatchGroupHooks = {\n\t\t\tonRepoChanged: function (_repoPath: string, changes: RepositoryChange[], ref?: string): void {\n\t\t\t\tsession.pushRepoChanges(changes, ref);\n\t\t\t},\n\t\t\tonFetchHeadChanged: (repoPath: string): void => {\n\t\t\t\t// Global first, so a host's cache drops its last-fetched time before any hook re-reads it\n\t\t\t\tthis._onDidChangeFetchHead.fire(repoPath);\n\t\t\t\t// Iterate at fire time so callers that joined after the group was wired up\n\t\t\t\t// still receive events (e.g. a `Repository.watch()` whose hooks landed\n\t\t\t\t// after a hookless `GitRepositoryService.watch` registered first).\n\t\t\t\tfor (const h of watchHooks) {\n\t\t\t\t\th.onFetchHeadChanged?.(repoPath);\n\t\t\t\t}\n\t\t\t},\n\t\t\tonIgnoresChanged: (repoPath: string): void => {\n\t\t\t\tthis.refreshWorkingTreeFilter(repoPath);\n\t\t\t\tfor (const h of watchHooks) {\n\t\t\t\t\th.onIgnoresChanged?.(repoPath);\n\t\t\t\t}\n\t\t\t},\n\t\t};\n\n\t\tgroup.addSession(session.repoPath, gitDir, hooks);\n\t}\n\n\tprivate disconnectFromWatchGroup(record: SessionRecord): void {\n\t\tconst { session, gitDir } = record;\n\t\tconst commonGitDir = (gitDir.commonUri ?? gitDir.uri).fsPath;\n\t\tconst group = this.watchGroups.get(commonGitDir);\n\t\tif (group == null) return;\n\n\t\tgroup.removeSession(session.repoPath);\n\t\tthis.maybeDisposeWatchGroup(commonGitDir);\n\t}\n\n\tprivate startWorkingTreeWatcher(record: SessionRecord): void {\n\t\tconst { session, gitDir, watchHooks } = record;\n\t\tconst repoPath = session.repoPath;\n\t\tif (this.wtWatchers.has(repoPath)) return;\n\n\t\tconst canPrune = this.canPruneIgnoredDirectories();\n\t\t// Assigned once the first watcher exists; read by the handlers below instead of a map lookup per event\n\t\tlet entry: WorkingTreeWatcherEntry | undefined;\n\n\t\tlet filter: GitIgnoreFilter | undefined;\n\t\tif (this.getIgnoreFilter != null) {\n\t\t\tfilter = this.getIgnoreFilter(repoPath, gitDir.uri.fsPath);\n\t\t}\n\n\t\t// Buffer events while the gitignore filter loads, then replay\n\t\tlet filterReady = filter == null;\n\t\tlet buffered: string[] | undefined = filter != null ? [] : undefined;\n\t\tlet bufferIncomplete = false;\n\n\t\tif (filter != null) {\n\t\t\tvoid filter\n\t\t\t\t.ready()\n\t\t\t\t.then(() => {\n\t\t\t\t\tfilterReady = true;\n\t\t\t\t\tif (buffered?.length) {\n\t\t\t\t\t\tconst paths = filterWorkingTreePaths(buffered, repoPath, filter);\n\t\t\t\t\t\tif (paths.length > 0 || bufferIncomplete) {\n\t\t\t\t\t\t\tsession.pushWorkingTreeChanges(paths, bufferIncomplete);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tbuffered = undefined;\n\t\t\t\t\tbufferIncomplete = false;\n\t\t\t\t})\n\t\t\t\t.catch(() => {\n\t\t\t\t\tfilterReady = true;\n\t\t\t\t\tbuffered = undefined;\n\t\t\t\t});\n\t\t}\n\n\t\t// An ignore rule changed (or may have): refresh the filter and the pruned set, and notify the host\n\t\tconst onIgnoreRulesChanged = (): void => {\n\t\t\tif (filter != null) {\n\t\t\t\tvoid filter.refresh();\n\t\t\t}\n\t\t\tif (entry != null) {\n\t\t\t\tthis.scheduleIgnoredDirectoriesRefresh(entry);\n\t\t\t}\n\t\t\t// See `connectToWatchGroup` for the iterate-at-fire-time rationale.\n\t\t\tfor (const h of watchHooks) {\n\t\t\t\th.onGitIgnoreChanged?.(repoPath);\n\t\t\t}\n\t\t\t// Route the ignore-rule change through the repo channel as an `ignores` change (like `info/exclude`),\n\t\t\t// so it reaches EVERY session subscriber — including a closed secondary worktree's `watch()` `repoSub`,\n\t\t\t// which has no `onGitIgnoreChanged` hook and would otherwise keep a stale untracked-file list. Coalesces\n\t\t\t// with any hook-fired `ignores` above, so open repos don't double.\n\t\t\tsession.pushRepoChanges(['ignores']);\n\t\t};\n\n\t\tconst onEvent = (event: FileWatchEvent): void => {\n\t\t\t// Dropped events: nothing to filter or buffer, the whole tree needs a re-read. A `.gitignore` edit may be\n\t\t\t// among them, and a filter left on the old rules would keep dropping changes they used to ignore\n\t\t\tif (event.reason === 'overflow') {\n\t\t\t\tsession.pushWorkingTreeChanges([], true);\n\t\t\t\tonIgnoreRulesChanged();\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Fast noise check (node_modules, .git, .watchman-cookie-)\n\t\t\tif (shouldIgnoreWorkingTreePath(event.path)) return;\n\n\t\t\t// Detect .gitignore changes (nested ones too, unless git skips them for sitting in an ignored directory).\n\t\t\t// The root .gitignore is only an ignore change; a nested one is also an ordinary working-tree change.\n\t\t\tconst relativePath = isDescendant(event.path, repoPath) ? relative(repoPath, event.path) : undefined;\n\t\t\tif (relativePath === '.gitignore') {\n\t\t\t\tonIgnoreRulesChanged();\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif (relativePath?.endsWith('/.gitignore') && !(filterReady && filter?.isIgnored(relativePath))) {\n\t\t\t\tonIgnoreRulesChanged();\n\t\t\t}\n\n\t\t\tif (!filterReady) {\n\t\t\t\t// Buffer until the gitignore filter is ready. Capped: `ready()` awaits a `core.excludesFile`\n\t\t\t\t// read, which queues behind every other git command, so this window can stay open for a long\n\t\t\t\t// time — and these paths haven't been gitignore-filtered yet, so a build's output all lands\n\t\t\t\t// here. Past the cap, stop retaining and mark the batch partial rather than growing.\n\t\t\t\tif (buffered != null && buffered.length >= maxBufferedWorkingTreePaths) {\n\t\t\t\t\tbufferIncomplete = true;\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\tbuffered?.push(event.path);\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Gitignore filter (sync after ready)\n\t\t\tif (filter != null && relativePath != null && filter.isIgnored(relativePath)) {\n\t\t\t\tif (canPrune && entry != null) {\n\t\t\t\t\tthis.onIgnoredEvent(entry, filter, relativePath);\n\t\t\t\t}\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tsession.pushWorkingTreeChanges([event.path]);\n\t\t};\n\n\t\tentry = {\n\t\t\tsession: session,\n\t\t\tonEvent: onEvent,\n\t\t\tfilter: filter,\n\t\t\twatcher: this.provider.createWatcher(repoPath, '**', onEvent, { excludes: workingTreeExcludes }),\n\t\t\tignoredDirectories: [],\n\t\t\tgeneration: 0,\n\t\t\treading: false,\n\t\t\trereadQueued: false,\n\t\t};\n\t\tthis.wtWatchers.set(repoPath, entry);\n\n\t\tif (canPrune) {\n\t\t\tvoid this.refreshIgnoredDirectories(entry, repoPath);\n\t\t}\n\t}\n\n\tprivate stopWorkingTreeWatcher(repoPath: string): void {\n\t\tconst entry = this.wtWatchers.get(repoPath);\n\t\tif (entry == null) return;\n\n\t\t// Drops any read still in flight, and any refresh waiting on its debounce\n\t\tentry.generation++;\n\t\tclearTimeout(entry.refreshTimer);\n\t\tentry.refreshTimer = undefined;\n\t\tentry.watcher.dispose();\n\t\tthis.wtWatchers.delete(repoPath);\n\t}\n\n\tprivate canPruneIgnoredDirectories(): boolean {\n\t\treturn this.provider.supportsExcludes === true && this.getIgnoredDirectories != null;\n\t}\n\n\t/**\n\t * An event the gitignore post-filter dropped. When its directory is itself ignored, the watcher is watching an\n\t * ignored directory it could prune (one created after the last read), so the ignored set is read again. An ignored\n\t * file in a watched directory (a root `.env`, `src/debug.log`) triggers nothing.\n\t */\n\tprivate onIgnoredEvent(entry: WorkingTreeWatcherEntry, filter: GitIgnoreFilter, relativePath: string): void {\n\t\t// A refresh is already pending and reads the whole set; keep a flood of these events at two field checks\n\t\tif (entry.refreshTimer != null || entry.rereadQueued) return;\n\n\t\t// The outermost ignored directory above the path — the one a read would prune. Keyed on it (not the immediate\n\t\t// parent), so a directory git won't prune costs one read however many subdirectories it has\n\t\tlet ignoredDir: string | undefined;\n\t\tfor (let slash = relativePath.indexOf('/'); slash !== -1; slash = relativePath.indexOf('/', slash + 1)) {\n\t\t\tconst dir = relativePath.slice(0, slash);\n\t\t\tif (entry.unpruned?.has(dir) || entry.unprunable?.has(dir)) return;\n\t\t\tif (filter.isIgnored(`${dir}/`)) {\n\t\t\t\tignoredDir = dir;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t\tif (ignoredDir == null) return;\n\n\t\tentry.unpruned ??= new Set();\n\t\tentry.unpruned.add(ignoredDir);\n\n\t\t// Ignore rules didn't change, so a read in flight stays valid (no generation bump), but it may predate the\n\t\t// directory\n\t\tif (entry.reading) {\n\t\t\tentry.rereadQueued = true;\n\t\t\treturn;\n\t\t}\n\n\t\tthis.armIgnoredDirectoriesRefresh(entry);\n\t}\n\n\t/** Re-reads the git-ignored directories once ignore rules stop changing (trailing debounce, one timer per repo) */\n\tprivate scheduleIgnoredDirectoriesRefresh(entry: WorkingTreeWatcherEntry): void {\n\t\tif (!this.canPruneIgnoredDirectories()) return;\n\n\t\t// A read already in flight is now stale, and a directory git wouldn't prune may be prunable now\n\t\tentry.generation++;\n\t\tentry.unprunable = undefined;\n\t\tclearTimeout(entry.refreshTimer);\n\t\tthis.armIgnoredDirectoriesRefresh(entry);\n\t}\n\n\tprivate armIgnoredDirectoriesRefresh(entry: WorkingTreeWatcherEntry): void {\n\t\tentry.refreshTimer = setTimeout(() => {\n\t\t\tentry.refreshTimer = undefined;\n\t\t\tconst repoPath = entry.session.repoPath;\n\t\t\tif (this.wtWatchers.get(repoPath) !== entry) return;\n\n\t\t\tif (entry.reading) {\n\t\t\t\tentry.rereadQueued = true;\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tvoid this.refreshIgnoredDirectories(entry, repoPath);\n\t\t}, ignoredDirectoriesRefreshDelayMs);\n\t}\n\n\t/** Single-flight: reads the git-ignored directories and, when they changed, swaps in a watcher excluding them */\n\tprivate async refreshIgnoredDirectories(entry: WorkingTreeWatcherEntry, repoPath: string): Promise<void> {\n\t\tif (this.getIgnoredDirectories == null) return;\n\n\t\tentry.reading = true;\n\t\ttry {\n\t\t\tconst generation = entry.generation;\n\t\t\t// The unpruned directories seen so far; any seen during the read wait for the next one\n\t\t\tconst unpruned = entry.unpruned?.size ? [...entry.unpruned] : undefined;\n\n\t\t\tlet directories: readonly string[] | undefined;\n\t\t\ttry {\n\t\t\t\tdirectories = await this.getIgnoredDirectories(repoPath);\n\t\t\t} catch {\n\t\t\t\t// Keep what the watcher has; the next ignore-rule change reads again\n\t\t\t\tdirectories = undefined;\n\t\t\t}\n\n\t\t\tif (entry.generation === generation) {\n\t\t\t\tif (directories != null) {\n\t\t\t\t\tthis.swapWorkingTreeWatcher(entry, repoPath, directories);\n\t\t\t\t}\n\t\t\t\tif (unpruned != null) {\n\t\t\t\t\tthis.settleUnprunedDirectories(entry, unpruned, directories);\n\t\t\t\t}\n\t\t\t}\n\t\t} finally {\n\t\t\tentry.reading = false;\n\t\t}\n\n\t\tif (entry.rereadQueued) {\n\t\t\tentry.rereadQueued = false;\n\t\t\tif (this.wtWatchers.get(repoPath) === entry) {\n\t\t\t\tvoid this.refreshIgnoredDirectories(entry, repoPath);\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * A directory the read left unexcluded is one git won't prune (or the read failed), so it never triggers a read\n\t * again until ignore rules change — otherwise each of its events would spawn git.\n\t */\n\tprivate settleUnprunedDirectories(\n\t\tentry: WorkingTreeWatcherEntry,\n\t\tunpruned: readonly string[],\n\t\tdirectories: readonly string[] | undefined,\n\t): void {\n\t\tfor (const dir of unpruned) {\n\t\t\tentry.unpruned?.delete(dir);\n\t\t\tif (directories?.some(d => d === dir || dir.startsWith(`${d}/`))) continue;\n\n\t\t\tentry.unprunable ??= new Set();\n\t\t\tentry.unprunable.add(dir);\n\t\t}\n\t}\n\n\tprivate swapWorkingTreeWatcher(\n\t\tentry: WorkingTreeWatcherEntry,\n\t\trepoPath: string,\n\t\tdirectories: readonly string[],\n\t): void {\n\t\tconst previous = entry.ignoredDirectories;\n\t\tif (previous.length === directories.length && previous.every(d => directories.includes(d))) return;\n\n\t\t// New first, then the old one: the overlap can repeat an event (the session coalesces them), but never loses one\n\t\tlet watcher: FileWatcher;\n\t\ttry {\n\t\t\twatcher = this.provider.createWatcher(repoPath, '**', entry.onEvent, {\n\t\t\t\texcludes: [...workingTreeExcludes, ...directories],\n\t\t\t});\n\t\t} catch {\n\t\t\treturn;\n\t\t}\n\n\t\tconst previousWatcher = entry.watcher;\n\t\tentry.watcher = watcher;\n\t\tentry.ignoredDirectories = directories;\n\t\tpreviousWatcher.dispose();\n\n\t\t// Files in a directory that is watched again changed while nothing was looking. A directory still covered by\n\t\t// a surviving ancestor entry isn't watched again.\n\t\tif (previous.some(d => !directories.some(n => n === d || d.startsWith(`${n}/`)))) {\n\t\t\tentry.session.pushWorkingTreeChanges([], true);\n\t\t}\n\t}\n\n\tprivate refreshWorkingTreeFilter(repoPath: string): void {\n\t\tconst entry = this.wtWatchers.get(repoPath);\n\t\tif (entry == null) return;\n\n\t\tif (entry.filter != null) {\n\t\t\tvoid entry.filter.refresh();\n\t\t}\n\t\tthis.scheduleIgnoredDirectoriesRefresh(entry);\n\t}\n\n\tprivate disposeSessionRecord(record: SessionRecord): void {\n\t\tthis.disconnectFromWatchGroup(record);\n\t\tthis.stopWorkingTreeWatcher(record.session.repoPath);\n\t\trecord.session.dispose();\n\t}\n}\n\nfunction filterWorkingTreePaths(absolutePaths: string[], repoPath: string, filter: GitIgnoreFilter): string[] {\n\tconst result: string[] = [];\n\tfor (const p of absolutePaths) {\n\t\tif (!isDescendant(p, repoPath)) continue;\n\n\t\tconst relativePath = relative(repoPath, p);\n\t\tif (!filter.isIgnored(relativePath)) {\n\t\t\tresult.push(p);\n\t\t}\n\t}\n\treturn result;\n}\n"],"mappings":";;;;;;;;;;;;AAoBA,MAAM,8BAA8B;;AAGpC,MAAM,mCAAmC;;;;;;;;;;;;AAwFzC,IAAa,yBAAb,MAAiE;CAChE,yBAA0C,IAAI,QAAgC;;CAE9E,IAAI,wBAAuD;EAC1D,OAAO,KAAK,uBAAuB;CACpC;CAEA,0BAA2C,IAAI,QAAgB;;;CAG/D,IAAI,yBAAwC;EAC3C,OAAO,KAAK,wBAAwB;CACrC;CAEA,wBAAyC,IAAI,QAAgB;;;;;;CAM7D,IAAI,uBAAsC;EACzC,OAAO,KAAK,sBAAsB;CACnC;CAEA;CACA;CACA;CACA;CACA;CACA;CACA;CAEA,6BAA8B,IAAI,IAA2B;CAC7D,8BAA+B,IAAI,IAAwB;CAC3D,6BAA8B,IAAI,IAAqC;CACvE,YAAoB;;CAEpB,aAAqB;CAErB,YAAY,SAA8B;EACzC,KAAK,WAAW,QAAQ;EACxB,KAAK,qBAAqB,QAAQ;EAClC,KAAK,4BAA4B,QAAQ;EACzC,KAAK,uBAAuB,QAAQ;EACpC,KAAK,8BAA8B,QAAQ;EAC3C,KAAK,kBAAkB,QAAQ;EAC/B,KAAK,wBAAwB,QAAQ;CACtC;CAEA,CAAC,OAAO,WAAiB;EACxB,KAAK,QAAQ;CACd;CAEA,UAAgB;EACf,IAAI,KAAK,WAAW;EAEpB,KAAK,YAAY;EAEjB,KAAK,MAAM,UAAU,KAAK,WAAW,OAAO,GAC3C,KAAK,qBAAqB,MAAM;EAEjC,KAAK,WAAW,MAAM;EAEtB,KAAK,MAAM,SAAS,KAAK,YAAY,OAAO,GAC3C,MAAM,QAAQ;EAEf,KAAK,YAAY,MAAM;EAEvB,KAAK,MAAM,YAAY,CAAC,GAAG,KAAK,WAAW,KAAK,CAAC,GAChD,KAAK,uBAAuB,QAAQ;EAGrC,KAAK,uBAAuB,QAAQ;EACpC,KAAK,wBAAwB,QAAQ;EACrC,KAAK,sBAAsB,QAAQ;CACpC;;;;;;;;;;;;;CAcA,MAAM,UAAkB,QAAgB,YAAkD;EACzF,IAAI,KAAK,WAAW,OAAO,KAAA;EAE3B,WAAW,iBAAiB,QAAQ;EAEpC,IAAI,SAAS,KAAK,WAAW,IAAI,QAAQ;EACzC,IAAI,UAAU,MAAM;GACnB,OAAO;GACP,IAAI,cAAc,MACjB,OAAO,WAAW,IAAI,UAAU;EAElC,OAAO;GACN,MAAM,UAAU,IAAI,uBAAuB;IAChC;IACV,oBAAoB,KAAK;IACzB,2BAA2B,KAAK;IAChC,sBAAsB,KAAK;IAC3B,6BAA6B,KAAK;IAClC,WAAW,KAAK;IAChB,WAAW;KACV,6BAAmC;MAClC,MAAM,MAAM,KAAK,WAAW,IAAI,QAAQ;MACxC,IAAI,OAAO,MACV,KAAK,oBAAoB,GAAG;KAE9B;KACA,4BAAkC;MACjC,MAAM,MAAM,KAAK,WAAW,IAAI,QAAQ;MACxC,IAAI,OAAO,MACV,KAAK,yBAAyB,GAAG;KAEnC;KACA,oCAA0C;MACzC,MAAM,MAAM,KAAK,WAAW,IAAI,QAAQ;MACxC,IAAI,OAAO,MACV,KAAK,wBAAwB,GAAG;KAElC;KACA,mCAAyC;MACxC,KAAK,uBAAuB,QAAQ;KACrC;IACD;IACA,sBAAsB,UAAgB;KACrC,KAAK,uBAAuB,KAAK,KAAK;IACvC;IACA,6BAA6B,aAAmB;KAC/C,KAAK,wBAAwB,KAAK,QAAQ;IAC3C;GACD,CAAC;GAED,MAAM,gCAAgB,IAAI,IAAgB;GAC1C,IAAI,cAAc,MACjB,cAAc,IAAI,UAAU;GAG7B,SAAS;IACC;IACD;IACR,YAAY;IACZ,UAAU;GACX;GACA,KAAK,WAAW,IAAI,UAAU,MAAM;EACrC;EAEA,MAAM,aAAa,OAAO;EAC1B,IAAI,iBAAiB;EAErB,OAAO;GACN,SAAS;GACT,eAAqB;IACpB,IAAI,gBAAgB;IAEpB,iBAAiB;IAEjB,MAAM,MAAM,KAAK,WAAW,IAAI,QAAQ;IACxC,IAAI,OAAO,MAAM;IAEjB,IAAI;IACJ,IAAI,cAAc,MACjB,IAAI,WAAW,OAAO,UAAU;IAEjC,IAAI,IAAI,YAAY,GAAG;KACtB,KAAK,WAAW,OAAO,QAAQ;KAC/B,KAAK,qBAAqB,GAAG;IAC9B;GACD;EACD;CACD;;CAGA,WAAW,UAAsD;EAChE,OAAO,KAAK,WAAW,IAAI,iBAAiB,QAAQ,CAAC,CAAC,EAAE;CACzD;;CAGA,mBAAkC;EACjC,OAAO;GACN,wBAAwB,KAAK,WAAW;GACxC,sBAAsB,KAAK,YAAY;GACvC,mCAAmC,KAAK,WAAW;EACpD;CACD;;CAGA,aAAmB;EAClB,KAAK,aAAa;EAClB,KAAK,MAAM,UAAU,KAAK,WAAW,OAAO,GAC3C,OAAO,QAAQ,QAAQ;CAEzB;;CAGA,UAAU,UAA8D;EACvE,KAAK,aAAa;EAClB,KAAK,MAAM,UAAU,KAAK,WAAW,OAAO,GAAG;GAC9C,MAAM,UAAU,WAAW,OAAO,OAAO;GACzC,OAAO,QAAQ,OAAO,OAAO;EAC9B;CACD;CAEA,sBAA8B,cAAkC;EAC/D,IAAI,QAAQ,KAAK,YAAY,IAAI,YAAY;EAC7C,IAAI,SAAS,MAAM;GAClB,QAAQ,IAAI,WAAW,cAAc,KAAK,QAAQ;GAClD,KAAK,YAAY,IAAI,cAAc,KAAK;EACzC;EACA,OAAO;CACR;CAEA,uBAA+B,cAA4B;EAC1D,MAAM,QAAQ,KAAK,YAAY,IAAI,YAAY;EAC/C,IAAI,SAAS,MAAM;EAEnB,IAAI,MAAM,SAAS,SAAS,GAAG;GAC9B,MAAM,QAAQ;GACd,KAAK,YAAY,OAAO,YAAY;EACrC;CACD;CAEA,oBAA4B,QAA6B;EACxD,MAAM,EAAE,SAAS,QAAQ,eAAe;EACxC,MAAM,gBAAgB,OAAO,aAAa,OAAO,IAAA,CAAK;EAyBtD,KAxBmB,sBAAsB,YAwBrC,CAAC,CAAC,WAAW,QAAQ,UAAU,QAAQ;GArB1C,eAAe,SAAU,WAAmB,SAA6B,KAAoB;IAC5F,QAAQ,gBAAgB,SAAS,GAAG;GACrC;GACA,qBAAqB,aAA2B;IAE/C,KAAK,sBAAsB,KAAK,QAAQ;IAIxC,KAAK,MAAM,KAAK,YACf,EAAE,qBAAqB,QAAQ;GAEjC;GACA,mBAAmB,aAA2B;IAC7C,KAAK,yBAAyB,QAAQ;IACtC,KAAK,MAAM,KAAK,YACf,EAAE,mBAAmB,QAAQ;GAE/B;EAG8C,CAAC;CACjD;CAEA,yBAAiC,QAA6B;EAC7D,MAAM,EAAE,SAAS,WAAW;EAC5B,MAAM,gBAAgB,OAAO,aAAa,OAAO,IAAA,CAAK;EACtD,MAAM,QAAQ,KAAK,YAAY,IAAI,YAAY;EAC/C,IAAI,SAAS,MAAM;EAEnB,MAAM,cAAc,QAAQ,QAAQ;EACpC,KAAK,uBAAuB,YAAY;CACzC;CAEA,wBAAgC,QAA6B;EAC5D,MAAM,EAAE,SAAS,QAAQ,eAAe;EACxC,MAAM,WAAW,QAAQ;EACzB,IAAI,KAAK,WAAW,IAAI,QAAQ,GAAG;EAEnC,MAAM,WAAW,KAAK,2BAA2B;EAEjD,IAAI;EAEJ,IAAI;EACJ,IAAI,KAAK,mBAAmB,MAC3B,SAAS,KAAK,gBAAgB,UAAU,OAAO,IAAI,MAAM;EAI1D,IAAI,cAAc,UAAU;EAC5B,IAAI,WAAiC,UAAU,OAAO,CAAC,IAAI,KAAA;EAC3D,IAAI,mBAAmB;EAEvB,IAAI,UAAU,MACb,OACE,MAAM,CAAC,CACP,WAAW;GACX,cAAc;GACd,IAAI,UAAU,QAAQ;IACrB,MAAM,QAAQ,uBAAuB,UAAU,UAAU,MAAM;IAC/D,IAAI,MAAM,SAAS,KAAK,kBACvB,QAAQ,uBAAuB,OAAO,gBAAgB;GAExD;GACA,WAAW,KAAA;GACX,mBAAmB;EACpB,CAAC,CAAC,CACD,YAAY;GACZ,cAAc;GACd,WAAW,KAAA;EACZ,CAAC;EAIH,MAAM,6BAAmC;GACxC,IAAI,UAAU,MACb,OAAY,QAAQ;GAErB,IAAI,SAAS,MACZ,KAAK,kCAAkC,KAAK;GAG7C,KAAK,MAAM,KAAK,YACf,EAAE,qBAAqB,QAAQ;GAMhC,QAAQ,gBAAgB,CAAC,SAAS,CAAC;EACpC;EAEA,MAAM,WAAW,UAAgC;GAGhD,IAAI,MAAM,WAAW,YAAY;IAChC,QAAQ,uBAAuB,CAAC,GAAG,IAAI;IACvC,qBAAqB;IACrB;GACD;GAGA,IAAI,4BAA4B,MAAM,IAAI,GAAG;GAI7C,MAAM,eAAe,aAAa,MAAM,MAAM,QAAQ,IAAI,SAAS,UAAU,MAAM,IAAI,IAAI,KAAA;GAC3F,IAAI,iBAAiB,cAAc;IAClC,qBAAqB;IACrB;GACD;GAEA,IAAI,cAAc,SAAS,aAAa,KAAK,EAAE,eAAe,QAAQ,UAAU,YAAY,IAC3F,qBAAqB;GAGtB,IAAI,CAAC,aAAa;IAKjB,IAAI,YAAY,QAAQ,SAAS,UAAU,6BAA6B;KACvE,mBAAmB;KACnB;IACD;IAEA,UAAU,KAAK,MAAM,IAAI;IACzB;GACD;GAGA,IAAI,UAAU,QAAQ,gBAAgB,QAAQ,OAAO,UAAU,YAAY,GAAG;IAC7E,IAAI,YAAY,SAAS,MACxB,KAAK,eAAe,OAAO,QAAQ,YAAY;IAEhD;GACD;GAEA,QAAQ,uBAAuB,CAAC,MAAM,IAAI,CAAC;EAC5C;EAEA,QAAQ;GACE;GACA;GACD;GACR,SAAS,KAAK,SAAS,cAAc,UAAU,MAAM,SAAS,EAAE,UAAU,oBAAoB,CAAC;GAC/F,oBAAoB,CAAC;GACrB,YAAY;GACZ,SAAS;GACT,cAAc;EACf;EACA,KAAK,WAAW,IAAI,UAAU,KAAK;EAEnC,IAAI,UACH,KAAU,0BAA0B,OAAO,QAAQ;CAErD;CAEA,uBAA+B,UAAwB;EACtD,MAAM,QAAQ,KAAK,WAAW,IAAI,QAAQ;EAC1C,IAAI,SAAS,MAAM;EAGnB,MAAM;EACN,aAAa,MAAM,YAAY;EAC/B,MAAM,eAAe,KAAA;EACrB,MAAM,QAAQ,QAAQ;EACtB,KAAK,WAAW,OAAO,QAAQ;CAChC;CAEA,6BAA8C;EAC7C,OAAO,KAAK,SAAS,qBAAqB,QAAQ,KAAK,yBAAyB;CACjF;;;;;;CAOA,eAAuB,OAAgC,QAAyB,cAA4B;EAE3G,IAAI,MAAM,gBAAgB,QAAQ,MAAM,cAAc;EAItD,IAAI;EACJ,KAAK,IAAI,QAAQ,aAAa,QAAQ,GAAG,GAAG,UAAU,IAAI,QAAQ,aAAa,QAAQ,KAAK,QAAQ,CAAC,GAAG;GACvG,MAAM,MAAM,aAAa,MAAM,GAAG,KAAK;GACvC,IAAI,MAAM,UAAU,IAAI,GAAG,KAAK,MAAM,YAAY,IAAI,GAAG,GAAG;GAC5D,IAAI,OAAO,UAAU,GAAG,IAAI,EAAE,GAAG;IAChC,aAAa;IACb;GACD;EACD;EACA,IAAI,cAAc,MAAM;EAExB,MAAM,6BAAa,IAAI,IAAI;EAC3B,MAAM,SAAS,IAAI,UAAU;EAI7B,IAAI,MAAM,SAAS;GAClB,MAAM,eAAe;GACrB;EACD;EAEA,KAAK,6BAA6B,KAAK;CACxC;;CAGA,kCAA0C,OAAsC;EAC/E,IAAI,CAAC,KAAK,2BAA2B,GAAG;EAGxC,MAAM;EACN,MAAM,aAAa,KAAA;EACnB,aAAa,MAAM,YAAY;EAC/B,KAAK,6BAA6B,KAAK;CACxC;CAEA,6BAAqC,OAAsC;EAC1E,MAAM,eAAe,iBAAiB;GACrC,MAAM,eAAe,KAAA;GACrB,MAAM,WAAW,MAAM,QAAQ;GAC/B,IAAI,KAAK,WAAW,IAAI,QAAQ,MAAM,OAAO;GAE7C,IAAI,MAAM,SAAS;IAClB,MAAM,eAAe;IACrB;GACD;GAEA,KAAU,0BAA0B,OAAO,QAAQ;EACpD,GAAG,gCAAgC;CACpC;;CAGA,MAAc,0BAA0B,OAAgC,UAAiC;EACxG,IAAI,KAAK,yBAAyB,MAAM;EAExC,MAAM,UAAU;EAChB,IAAI;GACH,MAAM,aAAa,MAAM;GAEzB,MAAM,WAAW,MAAM,UAAU,OAAO,CAAC,GAAG,MAAM,QAAQ,IAAI,KAAA;GAE9D,IAAI;GACJ,IAAI;IACH,cAAc,MAAM,KAAK,sBAAsB,QAAQ;GACxD,QAAQ;IAEP,cAAc,KAAA;GACf;GAEA,IAAI,MAAM,eAAe,YAAY;IACpC,IAAI,eAAe,MAClB,KAAK,uBAAuB,OAAO,UAAU,WAAW;IAEzD,IAAI,YAAY,MACf,KAAK,0BAA0B,OAAO,UAAU,WAAW;GAE7D;EACD,UAAU;GACT,MAAM,UAAU;EACjB;EAEA,IAAI,MAAM,cAAc;GACvB,MAAM,eAAe;GACrB,IAAI,KAAK,WAAW,IAAI,QAAQ,MAAM,OACrC,KAAU,0BAA0B,OAAO,QAAQ;EAErD;CACD;;;;;CAMA,0BACC,OACA,UACA,aACO;EACP,KAAK,MAAM,OAAO,UAAU;GAC3B,MAAM,UAAU,OAAO,GAAG;GAC1B,IAAI,aAAa,MAAK,MAAK,MAAM,OAAO,IAAI,WAAW,GAAG,EAAE,EAAE,CAAC,GAAG;GAElE,MAAM,+BAAe,IAAI,IAAI;GAC7B,MAAM,WAAW,IAAI,GAAG;EACzB;CACD;CAEA,uBACC,OACA,UACA,aACO;EACP,MAAM,WAAW,MAAM;EACvB,IAAI,SAAS,WAAW,YAAY,UAAU,SAAS,OAAM,MAAK,YAAY,SAAS,CAAC,CAAC,GAAG;EAG5F,IAAI;EACJ,IAAI;GACH,UAAU,KAAK,SAAS,cAAc,UAAU,MAAM,MAAM,SAAS,EACpE,UAAU,CAAC,GAAG,qBAAqB,GAAG,WAAW,EAClD,CAAC;EACF,QAAQ;GACP;EACD;EAEA,MAAM,kBAAkB,MAAM;EAC9B,MAAM,UAAU;EAChB,MAAM,qBAAqB;EAC3B,gBAAgB,QAAQ;EAIxB,IAAI,SAAS,MAAK,MAAK,CAAC,YAAY,MAAK,MAAK,MAAM,KAAK,EAAE,WAAW,GAAG,EAAE,EAAE,CAAC,CAAC,GAC9E,MAAM,QAAQ,uBAAuB,CAAC,GAAG,IAAI;CAE/C;CAEA,yBAAiC,UAAwB;EACxD,MAAM,QAAQ,KAAK,WAAW,IAAI,QAAQ;EAC1C,IAAI,SAAS,MAAM;EAEnB,IAAI,MAAM,UAAU,MACnB,MAAW,OAAO,QAAQ;EAE3B,KAAK,kCAAkC,KAAK;CAC7C;CAEA,qBAA6B,QAA6B;EACzD,KAAK,yBAAyB,MAAM;EACpC,KAAK,uBAAuB,OAAO,QAAQ,QAAQ;EACnD,OAAO,QAAQ,QAAQ;CACxB;AACD;AAEA,SAAS,uBAAuB,eAAyB,UAAkB,QAAmC;CAC7G,MAAM,SAAmB,CAAC;CAC1B,KAAK,MAAM,KAAK,eAAe;EAC9B,IAAI,CAAC,aAAa,GAAG,QAAQ,GAAG;EAEhC,MAAM,eAAe,SAAS,UAAU,CAAC;EACzC,IAAI,CAAC,OAAO,UAAU,YAAY,GACjC,OAAO,KAAK,CAAC;CAEf;CACA,OAAO;AACR"}
|