@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.
Files changed (83) hide show
  1. package/CHANGELOG.md +39 -1
  2. package/dist/git/cache.d.ts +34 -2
  3. package/dist/git/cache.js +55 -7
  4. package/dist/git/cache.js.map +1 -1
  5. package/dist/git/context.d.ts +1 -1
  6. package/dist/git/models/repository.d.ts +3 -2
  7. package/dist/git/models/repository.js +5 -4
  8. package/dist/git/models/repository.js.map +1 -1
  9. package/dist/git/providers/branches.d.ts +24 -0
  10. package/dist/git/providers/config.d.ts +3 -1
  11. package/dist/git/providers/provider.d.ts +13 -1
  12. package/dist/git/providers/staging.d.ts +1 -1
  13. package/dist/git/service.d.ts +17 -1
  14. package/dist/git/service.js +59 -8
  15. package/dist/git/service.js.map +1 -1
  16. package/dist/git/watching/changeEvent.d.ts +48 -5
  17. package/dist/git/watching/changeEvent.js +82 -7
  18. package/dist/git/watching/changeEvent.js.map +1 -1
  19. package/dist/git/watching/classifyChange.d.ts +13 -2
  20. package/dist/git/watching/classifyChange.js +40 -5
  21. package/dist/git/watching/classifyChange.js.map +1 -1
  22. package/dist/git/watching/gitIgnoreFilter.d.ts +57 -10
  23. package/dist/git/watching/gitIgnoreFilter.js +186 -29
  24. package/dist/git/watching/gitIgnoreFilter.js.map +1 -1
  25. package/dist/git/watching/initWatcher.d.ts +5 -1
  26. package/dist/git/watching/initWatcher.js +1 -1
  27. package/dist/git/watching/initWatcher.js.map +1 -1
  28. package/dist/git/watching/provider.d.ts +33 -8
  29. package/dist/git/watching/watchGroup.d.ts +13 -2
  30. package/dist/git/watching/watchGroup.js +60 -14
  31. package/dist/git/watching/watchGroup.js.map +1 -1
  32. package/dist/git/watching/watchService.d.ts +42 -1
  33. package/dist/git/watching/watchService.js +172 -20
  34. package/dist/git/watching/watchService.js.map +1 -1
  35. package/dist/git/watching/watchSession.d.ts +50 -17
  36. package/dist/git/watching/watchSession.js +274 -83
  37. package/dist/git/watching/watchSession.js.map +1 -1
  38. package/dist/git/watching/watcherPatterns.d.ts +13 -2
  39. package/dist/git/watching/watcherPatterns.js +22 -2
  40. package/dist/git/watching/watcherPatterns.js.map +1 -1
  41. package/dist/git/watching/worktreeSummary.d.ts +132 -0
  42. package/dist/git/watching/worktreeSummary.js +429 -0
  43. package/dist/git/watching/worktreeSummary.js.map +1 -0
  44. package/dist/git-cli/cliGitProvider.d.ts +6 -1
  45. package/dist/git-cli/cliGitProvider.js +87 -2
  46. package/dist/git-cli/cliGitProvider.js.map +1 -1
  47. package/dist/git-cli/providers/blame.js +1 -1
  48. package/dist/git-cli/providers/branches.d.ts +32 -1
  49. package/dist/git-cli/providers/branches.js +217 -0
  50. package/dist/git-cli/providers/branches.js.map +1 -1
  51. package/dist/git-cli/providers/commits.js +1 -1
  52. package/dist/git-cli/providers/config.d.ts +3 -0
  53. package/dist/git-cli/providers/config.js +12 -6
  54. package/dist/git-cli/providers/config.js.map +1 -1
  55. package/dist/git-cli/providers/contributors.js +1 -1
  56. package/dist/git-cli/providers/graph.js +1 -1
  57. package/dist/git-cli/providers/maintenance.js +1 -1
  58. package/dist/git-cli/providers/refs.js +1 -1
  59. package/dist/git-cli/providers/staging.js +1 -1
  60. package/dist/git-cli/providers/status.js +1 -1
  61. package/dist/git-cli/service.js +1 -0
  62. package/dist/git-cli/service.js.map +1 -1
  63. package/dist/ipc/ipcServer.js +1 -1
  64. package/dist/plus/agents/providers/gkAgentProvider.js +2 -2
  65. package/dist/plus/git-github/api/github.d.ts +1 -1
  66. package/dist/plus/git-github/api/github.js +1 -1
  67. package/dist/plus/git-github/providers/github/blame.js +1 -1
  68. package/dist/plus/integrations/authentication/integrationAuthenticationProvider.js +1 -1
  69. package/dist/plus/integrations/context.d.ts +1 -1
  70. package/dist/plus/integrations/integrationService.js +1 -1
  71. package/dist/plus/integrations/models/integration.js +1 -1
  72. package/dist/plus/integrations/providers/bitbucket-server.d.ts +1 -1
  73. package/dist/plus/integrations/reads/batchEtags.d.ts +5 -2
  74. package/dist/plus/integrations/reads/batchEtags.js +31 -10
  75. package/dist/plus/integrations/reads/batchEtags.js.map +1 -1
  76. package/dist/plus/integrations/reads/pullRequestBranches.js +10 -3
  77. package/dist/plus/integrations/reads/pullRequestBranches.js.map +1 -1
  78. package/dist/plus/integrations/results.d.ts +16 -1
  79. package/dist/plus/integrations/results.js +22 -3
  80. package/dist/plus/integrations/results.js.map +1 -1
  81. package/dist/utils/event.d.ts +1 -1
  82. package/docs/integrations.md +5 -0
  83. 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 { shouldIgnoreWorkingTreePath } from "./watcherPatterns.js";
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 entry of this.wtWatchers.values()) entry.watcher.dispose();
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: function(repoPath) {
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 watcher = this.provider.createWatcher(repoPath, "**", (event) => {
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
- if (filter != null) filter.refresh();
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 (relativePath != null && filter.isIgnored(relativePath)) return;
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
- this.wtWatchers.set(repoPath, {
248
- watcher,
249
- filter
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?.filter != null) entry.filter.refresh();
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"}