@gitkraken/core-gitlens 0.12.0 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (177) hide show
  1. package/CHANGELOG.md +57 -1
  2. package/dist/git/cache.d.ts +84 -29
  3. package/dist/git/cache.js +188 -54
  4. package/dist/git/cache.js.map +1 -1
  5. package/dist/git/features.d.ts +1 -1
  6. package/dist/git/features.js +4 -0
  7. package/dist/git/features.js.map +1 -1
  8. package/dist/git/models/branch.d.ts +15 -2
  9. package/dist/git/models/branch.js +27 -5
  10. package/dist/git/models/branch.js.map +1 -1
  11. package/dist/git/models/status.js +1 -1
  12. package/dist/git/models/worktree.d.ts +4 -1
  13. package/dist/git/models/worktree.js +4 -2
  14. package/dist/git/models/worktree.js.map +1 -1
  15. package/dist/git/providers/branches.d.ts +73 -8
  16. package/dist/git/providers/pausedOperations.d.ts +6 -0
  17. package/dist/git/providers/provider.d.ts +34 -2
  18. package/dist/git/providers/status.d.ts +8 -2
  19. package/dist/git/remotes/azure-devops.js +61 -53
  20. package/dist/git/remotes/azure-devops.js.map +1 -1
  21. package/dist/git/service.d.ts +8 -5
  22. package/dist/git/service.js +16 -8
  23. package/dist/git/service.js.map +1 -1
  24. package/dist/git/utils/branch.utils.d.ts +8 -1
  25. package/dist/git/utils/branch.utils.js +30 -1
  26. package/dist/git/utils/branch.utils.js.map +1 -1
  27. package/dist/git/watching/changeEvent.d.ts +13 -2
  28. package/dist/git/watching/changeEvent.js +17 -3
  29. package/dist/git/watching/changeEvent.js.map +1 -1
  30. package/dist/git/watching/gitDirPoller.d.ts +111 -0
  31. package/dist/git/watching/gitDirPoller.js +266 -0
  32. package/dist/git/watching/gitDirPoller.js.map +1 -0
  33. package/dist/git/watching/provider.d.ts +16 -0
  34. package/dist/git/watching/watchGroup.d.ts +40 -4
  35. package/dist/git/watching/watchGroup.js +143 -17
  36. package/dist/git/watching/watchGroup.js.map +1 -1
  37. package/dist/git/watching/watchService.d.ts +70 -1
  38. package/dist/git/watching/watchService.js +158 -13
  39. package/dist/git/watching/watchService.js.map +1 -1
  40. package/dist/git/watching/watchSession.d.ts +44 -0
  41. package/dist/git/watching/watchSession.js +91 -6
  42. package/dist/git/watching/watchSession.js.map +1 -1
  43. package/dist/git/watching/watcherPatterns.d.ts +21 -0
  44. package/dist/git/watching/watcherPatterns.js +67 -1
  45. package/dist/git/watching/watcherPatterns.js.map +1 -1
  46. package/dist/git/watching/worktreeSummary.d.ts +34 -7
  47. package/dist/git/watching/worktreeSummary.js +39 -12
  48. package/dist/git/watching/worktreeSummary.js.map +1 -1
  49. package/dist/git-cli/cliGitProvider.d.ts +34 -2
  50. package/dist/git-cli/cliGitProvider.js +119 -17
  51. package/dist/git-cli/cliGitProvider.js.map +1 -1
  52. package/dist/git-cli/exec/commandClassifier.d.ts +15 -0
  53. package/dist/git-cli/exec/commandClassifier.js +48 -2
  54. package/dist/git-cli/exec/commandClassifier.js.map +1 -1
  55. package/dist/git-cli/exec/git.d.ts +15 -3
  56. package/dist/git-cli/exec/git.js +38 -17
  57. package/dist/git-cli/exec/git.js.map +1 -1
  58. package/dist/git-cli/exec/gitQueue.d.ts +2 -0
  59. package/dist/git-cli/exec/gitQueue.js +2 -1
  60. package/dist/git-cli/exec/gitQueue.js.map +1 -1
  61. package/dist/git-cli/exec/indexLock.d.ts +38 -0
  62. package/dist/git-cli/exec/indexLock.js +150 -0
  63. package/dist/git-cli/exec/indexLock.js.map +1 -0
  64. package/dist/git-cli/parsers/worktreeParser.js +42 -2
  65. package/dist/git-cli/parsers/worktreeParser.js.map +1 -1
  66. package/dist/git-cli/providers/branches.d.ts +109 -10
  67. package/dist/git-cli/providers/branches.js +624 -131
  68. package/dist/git-cli/providers/branches.js.map +1 -1
  69. package/dist/git-cli/providers/config.d.ts +9 -5
  70. package/dist/git-cli/providers/config.js +60 -27
  71. package/dist/git-cli/providers/config.js.map +1 -1
  72. package/dist/git-cli/providers/operations.d.ts +17 -0
  73. package/dist/git-cli/providers/operations.js +153 -96
  74. package/dist/git-cli/providers/operations.js.map +1 -1
  75. package/dist/git-cli/providers/pausedOperations.d.ts +8 -0
  76. package/dist/git-cli/providers/pausedOperations.js +60 -40
  77. package/dist/git-cli/providers/pausedOperations.js.map +1 -1
  78. package/dist/git-cli/providers/refs.js +11 -8
  79. package/dist/git-cli/providers/refs.js.map +1 -1
  80. package/dist/git-cli/providers/remotes.js +19 -10
  81. package/dist/git-cli/providers/remotes.js.map +1 -1
  82. package/dist/git-cli/providers/staging.js +40 -15
  83. package/dist/git-cli/providers/staging.js.map +1 -1
  84. package/dist/git-cli/providers/stash.d.ts +2 -0
  85. package/dist/git-cli/providers/stash.js +33 -18
  86. package/dist/git-cli/providers/stash.js.map +1 -1
  87. package/dist/git-cli/providers/status.d.ts +3 -1
  88. package/dist/git-cli/providers/status.js +38 -20
  89. package/dist/git-cli/providers/status.js.map +1 -1
  90. package/dist/git-cli/providers/tags.js +6 -4
  91. package/dist/git-cli/providers/tags.js.map +1 -1
  92. package/dist/git-cli/providers/worktrees.d.ts +1 -0
  93. package/dist/git-cli/providers/worktrees.js +60 -31
  94. package/dist/git-cli/providers/worktrees.js.map +1 -1
  95. package/dist/git-cli/service.d.ts +6 -0
  96. package/dist/git-cli/service.js +5 -2
  97. package/dist/git-cli/service.js.map +1 -1
  98. package/dist/ipc/discovery.js +1 -1
  99. package/dist/plus/agents/providers/claudeCodeTranscript.js +6 -6
  100. package/dist/plus/agents/providers/claudeCodeTranscript.js.map +1 -1
  101. package/dist/plus/ai/providers/openAICompatibleProviderBase.js +1 -1
  102. package/dist/plus/git-github/providers/github/status.d.ts +3 -1
  103. package/dist/plus/git-github/providers/github/status.js +1 -1
  104. package/dist/plus/git-github/providers/github/status.js.map +1 -1
  105. package/dist/plus/integrations/collectionMetadata.d.ts +13 -2
  106. package/dist/plus/integrations/collectionMetadata.js +14 -2
  107. package/dist/plus/integrations/collectionMetadata.js.map +1 -1
  108. package/dist/plus/integrations/index.d.ts +2 -2
  109. package/dist/plus/integrations/index.js.map +1 -1
  110. package/dist/plus/integrations/integrationService.d.ts +1 -0
  111. package/dist/plus/integrations/integrationService.js +1 -1
  112. package/dist/plus/integrations/integrationService.js.map +1 -1
  113. package/dist/plus/integrations/manager.d.ts +1 -0
  114. package/dist/plus/integrations/models/gitHostIntegration.d.ts +41 -5
  115. package/dist/plus/integrations/models/gitHostIntegration.js +106 -29
  116. package/dist/plus/integrations/models/gitHostIntegration.js.map +1 -1
  117. package/dist/plus/integrations/models/issueReads.d.ts +17 -4
  118. package/dist/plus/integrations/providers/apiClients.d.ts +2 -0
  119. package/dist/plus/integrations/providers/apiClients.js +7 -1
  120. package/dist/plus/integrations/providers/apiClients.js.map +1 -1
  121. package/dist/plus/integrations/providers/azure/azure.d.ts +5 -4
  122. package/dist/plus/integrations/providers/azure/azure.js +37 -77
  123. package/dist/plus/integrations/providers/azure/azure.js.map +1 -1
  124. package/dist/plus/integrations/providers/azure/azure.utils.js +20 -17
  125. package/dist/plus/integrations/providers/azure/azure.utils.js.map +1 -1
  126. package/dist/plus/integrations/providers/azure/models.d.ts +2 -2
  127. package/dist/plus/integrations/providers/azure/models.js +4 -4
  128. package/dist/plus/integrations/providers/azure/models.js.map +1 -1
  129. package/dist/plus/integrations/providers/azure/workItemStateCategories.d.ts +18 -14
  130. package/dist/plus/integrations/providers/azure/workItemStateCategories.js +69 -17
  131. package/dist/plus/integrations/providers/azure/workItemStateCategories.js.map +1 -1
  132. package/dist/plus/integrations/providers/azureDevOps.d.ts +44 -0
  133. package/dist/plus/integrations/providers/azureDevOps.js +108 -17
  134. package/dist/plus/integrations/providers/azureDevOps.js.map +1 -1
  135. package/dist/plus/integrations/providers/azureDevOpsServer.d.ts +45 -1
  136. package/dist/plus/integrations/providers/azureDevOpsServer.js +121 -6
  137. package/dist/plus/integrations/providers/azureDevOpsServer.js.map +1 -1
  138. package/dist/plus/integrations/providers/jira-server.js +18 -14
  139. package/dist/plus/integrations/providers/jira-server.js.map +1 -1
  140. package/dist/plus/integrations/providers/jira.js +18 -14
  141. package/dist/plus/integrations/providers/jira.js.map +1 -1
  142. package/dist/plus/integrations/providers/models.d.ts +1 -2
  143. package/dist/plus/integrations/providers/models.js +9 -2
  144. package/dist/plus/integrations/providers/models.js.map +1 -1
  145. package/dist/plus/integrations/providers/providersApi.d.ts +7 -17
  146. package/dist/plus/integrations/providers/providersApi.js +85 -78
  147. package/dist/plus/integrations/providers/providersApi.js.map +1 -1
  148. package/dist/plus/integrations/providers/utils/jiraIssueDrains.d.ts +22 -0
  149. package/dist/plus/integrations/providers/utils/jiraIssueDrains.js +69 -9
  150. package/dist/plus/integrations/providers/utils/jiraIssueDrains.js.map +1 -1
  151. package/dist/plus/integrations/providers/utils/providerPaging.js +8 -2
  152. package/dist/plus/integrations/providers/utils/providerPaging.js.map +1 -1
  153. package/dist/plus/integrations/reads/filters.d.ts +3 -3
  154. package/dist/plus/integrations/reads/filters.js.map +1 -1
  155. package/dist/plus/integrations/reads/issueBatch.d.ts +2 -1
  156. package/dist/plus/integrations/reads/issueBatch.js +1 -1
  157. package/dist/plus/integrations/reads/issueBatch.js.map +1 -1
  158. package/dist/plus/integrations/reads/issueTracker.js +23 -14
  159. package/dist/plus/integrations/reads/issueTracker.js.map +1 -1
  160. package/dist/plus/integrations/reads/issues.d.ts +2 -1
  161. package/dist/plus/integrations/reads/issues.js +18 -3
  162. package/dist/plus/integrations/reads/issues.js.map +1 -1
  163. package/dist/plus/integrations/reads/pullRequestBatch.js +1 -1
  164. package/dist/plus/integrations/reads/resolveRepository.js +12 -3
  165. package/dist/plus/integrations/reads/resolveRepository.js.map +1 -1
  166. package/dist/plus/integrations/reads/warnings.d.ts +11 -4
  167. package/dist/plus/integrations/reads/warnings.js +15 -5
  168. package/dist/plus/integrations/reads/warnings.js.map +1 -1
  169. package/dist/plus/integrations/results.d.ts +36 -2
  170. package/dist/plus/integrations/results.js +33 -13
  171. package/dist/plus/integrations/results.js.map +1 -1
  172. package/dist/utils/env/node/exec.js +2 -2
  173. package/dist/utils/promiseCache.d.ts +10 -0
  174. package/dist/utils/promiseCache.js +17 -2
  175. package/dist/utils/promiseCache.js.map +1 -1
  176. package/docs/integrations.md +52 -20
  177. package/package.json +2 -2
@@ -27,12 +27,23 @@ export declare class WatcherRepoChangeEvent {
27
27
  protected readonly _changes: Set<RepositoryChange>;
28
28
  private readonly _refs;
29
29
  private readonly _unnamedRefKinds;
30
+ private readonly _announced;
30
31
  constructor(
31
32
  /** The repository path this event is for */
32
33
  repoPath: string, changes: RepositoryChange[],
33
34
  /** Which refs changed; when omitted, every ref kind in `changes` is unnamed (any ref of that kind may have moved) */
34
- refs?: WatcherRefChanges);
35
+ refs?: WatcherRefChanges,
36
+ /** The kinds of `changes` that come only from core's own announced writes (see {@link announced}) */
37
+ announced?: Iterable<RepositoryChange>);
35
38
  get changes(): ReadonlySet<RepositoryChange>;
39
+ /**
40
+ * The kinds in {@link changes} that come only from core's own writes, which core already announced through
41
+ * `hooks.repository.onChanged` after evicting their caches. A host that handled that announcement can skip them;
42
+ * a kind the watcher or an injected change also reported is not among them.
43
+ */
44
+ get announced(): ReadonlySet<RepositoryChange>;
45
+ /** The kinds in {@link changes} that aren't {@link announced}: what a cache that core already evicted for its write still has to clear */
46
+ getUnannouncedChanges(): RepositoryChange[];
36
47
  /** Full ref names (or ref directories, or pseudo-refs) known to have changed. See {@link WatcherRefChanges.names}. */
37
48
  get refs(): ReadonlySet<string>;
38
49
  /** Ref kinds that changed without a name — any ref of these kinds may have moved */
@@ -66,7 +77,7 @@ export declare class WatcherRepoChangeEvent {
66
77
  changedExclusive(...affected: RepositoryChange[]): boolean;
67
78
  /**
68
79
  * Coalesce: return a new event with all changes and refs from both this and the given set. When `refs` is omitted,
69
- * every ref kind in `changes` counts as unnamed.
80
+ * every ref kind in `changes` counts as unnamed. A kind in `changes` is no longer {@link announced}.
70
81
  */
71
82
  with(changes: RepositoryChange[], refs?: WatcherRefChanges): WatcherRepoChangeEvent;
72
83
  }
@@ -26,7 +26,8 @@ var WatcherRepoChangeEvent = class WatcherRepoChangeEvent {
26
26
  _changes;
27
27
  _refs;
28
28
  _unnamedRefKinds;
29
- constructor(repoPath, changes, refs) {
29
+ _announced;
30
+ constructor(repoPath, changes, refs, announced) {
30
31
  this.repoPath = repoPath;
31
32
  this._changes = new Set(changes);
32
33
  if (refs == null) {
@@ -36,10 +37,23 @@ var WatcherRepoChangeEvent = class WatcherRepoChangeEvent {
36
37
  this._refs = new Set(refs.names);
37
38
  this._unnamedRefKinds = new Set(refs.unnamed);
38
39
  }
40
+ this._announced = announced != null ? new Set(filter(announced, (c) => this._changes.has(c))) : /* @__PURE__ */ new Set();
39
41
  }
40
42
  get changes() {
41
43
  return this._changes;
42
44
  }
45
+ /**
46
+ * The kinds in {@link changes} that come only from core's own writes, which core already announced through
47
+ * `hooks.repository.onChanged` after evicting their caches. A host that handled that announcement can skip them;
48
+ * a kind the watcher or an injected change also reported is not among them.
49
+ */
50
+ get announced() {
51
+ return this._announced;
52
+ }
53
+ /** The kinds in {@link changes} that aren't {@link announced}: what a cache that core already evicted for its write still has to clear */
54
+ getUnannouncedChanges() {
55
+ return this._announced.size ? [...filter(this._changes, (c) => !this._announced.has(c))] : [...this._changes];
56
+ }
43
57
  /** Full ref names (or ref directories, or pseudo-refs) known to have changed. See {@link WatcherRefChanges.names}. */
44
58
  get refs() {
45
59
  return this._refs;
@@ -101,7 +115,7 @@ var WatcherRepoChangeEvent = class WatcherRepoChangeEvent {
101
115
  }
102
116
  /**
103
117
  * Coalesce: return a new event with all changes and refs from both this and the given set. When `refs` is omitted,
104
- * every ref kind in `changes` counts as unnamed.
118
+ * every ref kind in `changes` counts as unnamed. A kind in `changes` is no longer {@link announced}.
105
119
  */
106
120
  with(changes, refs) {
107
121
  const names = [...this._refs];
@@ -114,7 +128,7 @@ var WatcherRepoChangeEvent = class WatcherRepoChangeEvent {
114
128
  return new WatcherRepoChangeEvent(this.repoPath, [...this._changes, ...changes], {
115
129
  names,
116
130
  unnamed
117
- });
131
+ }, filter(this._announced, (c) => !changes.includes(c)));
118
132
  }
119
133
  };
120
134
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"changeEvent.js","names":[],"sources":["../../../../git/src/watching/changeEvent.ts"],"sourcesContent":["import { filter, some } from '@gitlens/utils/iterable.js';\nimport type { RepositoryChange } from '../models/repository.js';\n\n/** The {@link RepositoryChange} kinds that describe ref movement, which an event can attribute to named refs */\nexport type RefChangeKind = 'heads' | 'remotes' | 'tags' | 'stash';\n\nexport interface WatcherRefChanges {\n\t/** Full ref names whose files changed (`refs/heads/main`), plus per-worktree pseudo-refs (`HEAD`, `ORIG_HEAD`). A name may be a ref directory and then covers every ref beneath it. */\n\treadonly names?: Iterable<string>;\n\t/** Ref kinds that changed without a name (packed-refs rewrite, config, shallow/replace, an injected change): any ref of that kind may have moved. */\n\treadonly unnamed?: Iterable<RefChangeKind>;\n}\n\nexport function isRefChangeKind(change: RepositoryChange): change is RefChangeKind {\n\treturn change === 'heads' || change === 'remotes' || change === 'tags' || change === 'stash';\n}\n\n/**\n * The kind of ref a full ref name (or ref directory) belongs to. A pseudo-ref (no `refs/` prefix, e.g. `HEAD`,\n * `ORIG_HEAD`) counts as `heads`; a ref outside the watched namespaces (e.g. `refs/notes/x`) has no kind.\n */\nexport function getRefChangeKind(ref: string): RefChangeKind | undefined {\n\tif (!ref.startsWith('refs/')) return 'heads';\n\tif (ref.startsWith('refs/heads/')) return 'heads';\n\tif (ref.startsWith('refs/remotes/')) return 'remotes';\n\tif (ref.startsWith('refs/tags/')) return 'tags';\n\tif (ref === 'refs/stash') return 'stash';\n\n\treturn undefined;\n}\n\n/**\n * An event describing changes to a repository, produced by the watching pipeline.\n *\n * Changes are coalesced within a debounce window — a single event may represent\n * multiple filesystem changes. Use {@link changed} to test for specific change types,\n * and {@link mayHaveChangedRef} to test whether a specific ref may have moved.\n */\nexport class WatcherRepoChangeEvent {\n\tprotected readonly _changes: Set<RepositoryChange>;\n\tprivate readonly _refs: Set<string>;\n\tprivate readonly _unnamedRefKinds: Set<RefChangeKind>;\n\n\tconstructor(\n\t\t/** The repository path this event is for */\n\t\tpublic readonly repoPath: string,\n\t\tchanges: RepositoryChange[],\n\t\t/** Which refs changed; when omitted, every ref kind in `changes` is unnamed (any ref of that kind may have moved) */\n\t\trefs?: WatcherRefChanges,\n\t) {\n\t\tthis._changes = new Set(changes);\n\t\tif (refs == null) {\n\t\t\tthis._refs = new Set();\n\t\t\tthis._unnamedRefKinds = new Set(filter(changes, isRefChangeKind));\n\t\t} else {\n\t\t\tthis._refs = new Set(refs.names);\n\t\t\tthis._unnamedRefKinds = new Set(refs.unnamed);\n\t\t}\n\t}\n\n\tget changes(): ReadonlySet<RepositoryChange> {\n\t\treturn this._changes;\n\t}\n\n\t/** Full ref names (or ref directories, or pseudo-refs) known to have changed. See {@link WatcherRefChanges.names}. */\n\tget refs(): ReadonlySet<string> {\n\t\treturn this._refs;\n\t}\n\n\t/** Ref kinds that changed without a name — any ref of these kinds may have moved */\n\tget unnamedRefKinds(): ReadonlySet<RefChangeKind> {\n\t\treturn this._unnamedRefKinds;\n\t}\n\n\ttoString(changesOnly: boolean = false): string {\n\t\tconst changeList = [...this._changes].join(', ');\n\t\tconst refCount = this._refs.size;\n\t\tif (changesOnly) return refCount ? `changes=${changeList}, refs=${refCount}` : `changes=${changeList}`;\n\n\t\treturn refCount\n\t\t\t? `{ repoPath: ${this.repoPath}, changes: ${changeList}, refs: ${refCount} }`\n\t\t\t: `{ repoPath: ${this.repoPath}, changes: ${changeList} }`;\n\t}\n\n\t/**\n\t * Test whether the given ref may have moved in this event.\n\t *\n\t * @param ref - A full ref name (`refs/heads/main`) or a pseudo-ref (`HEAD`).\n\t * @returns `false` when the ref's kind didn't change (or the ref is outside the watched kinds); `true` when its kind\n\t * changed without a name, or when the ref itself or a ref directory above it changed. `HEAD` is usually a symref\n\t * to the checked-out branch, so it may have moved whenever any `heads` ref did.\n\t */\n\tmayHaveChangedRef(ref: string): boolean {\n\t\tconst kind = getRefChangeKind(ref);\n\t\tif (kind == null || !this._changes.has(kind)) return false;\n\t\tif (this._unnamedRefKinds.has(kind) || ref === 'HEAD') return true;\n\n\t\treturn findCoveringRefName(this._refs, ref) != null;\n\t}\n\n\t/**\n\t * Test whether any of the specified change types are present in the event.\n\t *\n\t * @param affected - Change types to test for.\n\t * @returns `true` if any of the specified changes are present.\n\t */\n\tchanged(...affected: RepositoryChange[]): boolean {\n\t\treturn some(this._changes, c => affected.includes(c));\n\t}\n\n\t/**\n\t * Test whether the event contains ONLY the specified change types.\n\t *\n\t * `pausedOp` is treated as a union of `cherryPick`, `merge`, `rebase`,\n\t * and `revert` for convenience.\n\t *\n\t * @param affected - Change types to test for.\n\t * @returns `true` if the event contains only the specified changes.\n\t */\n\tchangedExclusive(...affected: RepositoryChange[]): boolean {\n\t\tlet changes = this._changes;\n\n\t\t// When checking for specific paused operation types, also accept the union type\n\t\tif (\n\t\t\taffected.includes('cherryPick') ||\n\t\t\taffected.includes('merge') ||\n\t\t\taffected.includes('rebase') ||\n\t\t\taffected.includes('revert')\n\t\t) {\n\t\t\tif (!affected.includes('pausedOp')) {\n\t\t\t\taffected = [...affected, 'pausedOp'];\n\t\t\t}\n\t\t} else if (affected.includes('pausedOp')) {\n\t\t\t// When checking for the union type exclusively, ignore specific subtypes\n\t\t\tchanges = new Set(changes);\n\t\t\tchanges.delete('cherryPick');\n\t\t\tchanges.delete('merge');\n\t\t\tchanges.delete('rebase');\n\t\t\tchanges.delete('revert');\n\t\t}\n\n\t\tconst intersection = [...filter(changes, c => affected.includes(c))];\n\t\treturn intersection.length === changes.size;\n\t}\n\n\t/**\n\t * Coalesce: return a new event with all changes and refs from both this and the given set. When `refs` is omitted,\n\t * every ref kind in `changes` counts as unnamed.\n\t */\n\twith(changes: RepositoryChange[], refs?: WatcherRefChanges): WatcherRepoChangeEvent {\n\t\tconst names = [...this._refs];\n\t\tconst unnamed = [...this._unnamedRefKinds];\n\t\tif (refs == null) {\n\t\t\tunnamed.push(...filter(changes, isRefChangeKind));\n\t\t} else {\n\t\t\tif (refs.names != null) {\n\t\t\t\tnames.push(...refs.names);\n\t\t\t}\n\t\t\tif (refs.unnamed != null) {\n\t\t\t\tunnamed.push(...refs.unnamed);\n\t\t\t}\n\t\t}\n\n\t\treturn new WatcherRepoChangeEvent(this.repoPath, [...this._changes, ...changes], {\n\t\t\tnames: names,\n\t\t\tunnamed: unnamed,\n\t\t});\n\t}\n}\n\n/**\n * Returns the name in `names` that covers `ref` — `ref` itself, or a ref directory above it (compared on `/`\n * boundaries, so `refs/heads/fe` doesn't cover `refs/heads/feature`) — or `undefined` if none does.\n */\nexport function findCoveringRefName(names: ReadonlySet<string>, ref: string): string | undefined {\n\tif (names.size === 0) return undefined;\n\tif (names.has(ref)) return ref;\n\n\tlet index = ref.lastIndexOf('/');\n\twhile (index > 0) {\n\t\tconst dir = ref.substring(0, index);\n\t\tif (names.has(dir)) return dir;\n\n\t\tindex = ref.lastIndexOf('/', index - 1);\n\t}\n\treturn undefined;\n}\n\n/**\n * An event describing working tree file changes, produced by the working tree pipeline.\n *\n * Paths have already been filtered through the gitignore filter and noise filter.\n */\nexport interface WorkingTreeChangeEvent {\n\treadonly repoPath: string;\n\t/**\n\t * Absolute paths of changed files or directories (post-gitignore filtering). A directory stands for everything\n\t * beneath it. Partial when {@link incomplete}.\n\t */\n\treadonly paths: ReadonlySet<string>;\n\t/**\n\t * `true` when more paths changed than the accumulator retains, so {@link paths} is a partial set and a\n\t * miss does NOT mean the file is unchanged. Consumers testing membership must treat this as a match.\n\t */\n\treadonly incomplete: boolean;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"changeEvent.js","names":[],"sources":["../../../../git/src/watching/changeEvent.ts"],"sourcesContent":["import { filter, some } from '@gitlens/utils/iterable.js';\nimport type { RepositoryChange } from '../models/repository.js';\n\n/** The {@link RepositoryChange} kinds that describe ref movement, which an event can attribute to named refs */\nexport type RefChangeKind = 'heads' | 'remotes' | 'tags' | 'stash';\n\nexport interface WatcherRefChanges {\n\t/** Full ref names whose files changed (`refs/heads/main`), plus per-worktree pseudo-refs (`HEAD`, `ORIG_HEAD`). A name may be a ref directory and then covers every ref beneath it. */\n\treadonly names?: Iterable<string>;\n\t/** Ref kinds that changed without a name (packed-refs rewrite, config, shallow/replace, an injected change): any ref of that kind may have moved. */\n\treadonly unnamed?: Iterable<RefChangeKind>;\n}\n\nexport function isRefChangeKind(change: RepositoryChange): change is RefChangeKind {\n\treturn change === 'heads' || change === 'remotes' || change === 'tags' || change === 'stash';\n}\n\n/**\n * The kind of ref a full ref name (or ref directory) belongs to. A pseudo-ref (no `refs/` prefix, e.g. `HEAD`,\n * `ORIG_HEAD`) counts as `heads`; a ref outside the watched namespaces (e.g. `refs/notes/x`) has no kind.\n */\nexport function getRefChangeKind(ref: string): RefChangeKind | undefined {\n\tif (!ref.startsWith('refs/')) return 'heads';\n\tif (ref.startsWith('refs/heads/')) return 'heads';\n\tif (ref.startsWith('refs/remotes/')) return 'remotes';\n\tif (ref.startsWith('refs/tags/')) return 'tags';\n\tif (ref === 'refs/stash') return 'stash';\n\n\treturn undefined;\n}\n\n/**\n * An event describing changes to a repository, produced by the watching pipeline.\n *\n * Changes are coalesced within a debounce window — a single event may represent\n * multiple filesystem changes. Use {@link changed} to test for specific change types,\n * and {@link mayHaveChangedRef} to test whether a specific ref may have moved.\n */\nexport class WatcherRepoChangeEvent {\n\tprotected readonly _changes: Set<RepositoryChange>;\n\tprivate readonly _refs: Set<string>;\n\tprivate readonly _unnamedRefKinds: Set<RefChangeKind>;\n\tprivate readonly _announced: Set<RepositoryChange>;\n\n\tconstructor(\n\t\t/** The repository path this event is for */\n\t\tpublic readonly repoPath: string,\n\t\tchanges: RepositoryChange[],\n\t\t/** Which refs changed; when omitted, every ref kind in `changes` is unnamed (any ref of that kind may have moved) */\n\t\trefs?: WatcherRefChanges,\n\t\t/** The kinds of `changes` that come only from core's own announced writes (see {@link announced}) */\n\t\tannounced?: Iterable<RepositoryChange>,\n\t) {\n\t\tthis._changes = new Set(changes);\n\t\tif (refs == null) {\n\t\t\tthis._refs = new Set();\n\t\t\tthis._unnamedRefKinds = new Set(filter(changes, isRefChangeKind));\n\t\t} else {\n\t\t\tthis._refs = new Set(refs.names);\n\t\t\tthis._unnamedRefKinds = new Set(refs.unnamed);\n\t\t}\n\t\tthis._announced = announced != null ? new Set(filter(announced, c => this._changes.has(c))) : new Set();\n\t}\n\n\tget changes(): ReadonlySet<RepositoryChange> {\n\t\treturn this._changes;\n\t}\n\n\t/**\n\t * The kinds in {@link changes} that come only from core's own writes, which core already announced through\n\t * `hooks.repository.onChanged` after evicting their caches. A host that handled that announcement can skip them;\n\t * a kind the watcher or an injected change also reported is not among them.\n\t */\n\tget announced(): ReadonlySet<RepositoryChange> {\n\t\treturn this._announced;\n\t}\n\n\t/** The kinds in {@link changes} that aren't {@link announced}: what a cache that core already evicted for its write still has to clear */\n\tgetUnannouncedChanges(): RepositoryChange[] {\n\t\treturn this._announced.size ? [...filter(this._changes, c => !this._announced.has(c))] : [...this._changes];\n\t}\n\n\t/** Full ref names (or ref directories, or pseudo-refs) known to have changed. See {@link WatcherRefChanges.names}. */\n\tget refs(): ReadonlySet<string> {\n\t\treturn this._refs;\n\t}\n\n\t/** Ref kinds that changed without a name — any ref of these kinds may have moved */\n\tget unnamedRefKinds(): ReadonlySet<RefChangeKind> {\n\t\treturn this._unnamedRefKinds;\n\t}\n\n\ttoString(changesOnly: boolean = false): string {\n\t\tconst changeList = [...this._changes].join(', ');\n\t\tconst refCount = this._refs.size;\n\t\tif (changesOnly) return refCount ? `changes=${changeList}, refs=${refCount}` : `changes=${changeList}`;\n\n\t\treturn refCount\n\t\t\t? `{ repoPath: ${this.repoPath}, changes: ${changeList}, refs: ${refCount} }`\n\t\t\t: `{ repoPath: ${this.repoPath}, changes: ${changeList} }`;\n\t}\n\n\t/**\n\t * Test whether the given ref may have moved in this event.\n\t *\n\t * @param ref - A full ref name (`refs/heads/main`) or a pseudo-ref (`HEAD`).\n\t * @returns `false` when the ref's kind didn't change (or the ref is outside the watched kinds); `true` when its kind\n\t * changed without a name, or when the ref itself or a ref directory above it changed. `HEAD` is usually a symref\n\t * to the checked-out branch, so it may have moved whenever any `heads` ref did.\n\t */\n\tmayHaveChangedRef(ref: string): boolean {\n\t\tconst kind = getRefChangeKind(ref);\n\t\tif (kind == null || !this._changes.has(kind)) return false;\n\t\tif (this._unnamedRefKinds.has(kind) || ref === 'HEAD') return true;\n\n\t\treturn findCoveringRefName(this._refs, ref) != null;\n\t}\n\n\t/**\n\t * Test whether any of the specified change types are present in the event.\n\t *\n\t * @param affected - Change types to test for.\n\t * @returns `true` if any of the specified changes are present.\n\t */\n\tchanged(...affected: RepositoryChange[]): boolean {\n\t\treturn some(this._changes, c => affected.includes(c));\n\t}\n\n\t/**\n\t * Test whether the event contains ONLY the specified change types.\n\t *\n\t * `pausedOp` is treated as a union of `cherryPick`, `merge`, `rebase`,\n\t * and `revert` for convenience.\n\t *\n\t * @param affected - Change types to test for.\n\t * @returns `true` if the event contains only the specified changes.\n\t */\n\tchangedExclusive(...affected: RepositoryChange[]): boolean {\n\t\tlet changes = this._changes;\n\n\t\t// When checking for specific paused operation types, also accept the union type\n\t\tif (\n\t\t\taffected.includes('cherryPick') ||\n\t\t\taffected.includes('merge') ||\n\t\t\taffected.includes('rebase') ||\n\t\t\taffected.includes('revert')\n\t\t) {\n\t\t\tif (!affected.includes('pausedOp')) {\n\t\t\t\taffected = [...affected, 'pausedOp'];\n\t\t\t}\n\t\t} else if (affected.includes('pausedOp')) {\n\t\t\t// When checking for the union type exclusively, ignore specific subtypes\n\t\t\tchanges = new Set(changes);\n\t\t\tchanges.delete('cherryPick');\n\t\t\tchanges.delete('merge');\n\t\t\tchanges.delete('rebase');\n\t\t\tchanges.delete('revert');\n\t\t}\n\n\t\tconst intersection = [...filter(changes, c => affected.includes(c))];\n\t\treturn intersection.length === changes.size;\n\t}\n\n\t/**\n\t * Coalesce: return a new event with all changes and refs from both this and the given set. When `refs` is omitted,\n\t * every ref kind in `changes` counts as unnamed. A kind in `changes` is no longer {@link announced}.\n\t */\n\twith(changes: RepositoryChange[], refs?: WatcherRefChanges): WatcherRepoChangeEvent {\n\t\tconst names = [...this._refs];\n\t\tconst unnamed = [...this._unnamedRefKinds];\n\t\tif (refs == null) {\n\t\t\tunnamed.push(...filter(changes, isRefChangeKind));\n\t\t} else {\n\t\t\tif (refs.names != null) {\n\t\t\t\tnames.push(...refs.names);\n\t\t\t}\n\t\t\tif (refs.unnamed != null) {\n\t\t\t\tunnamed.push(...refs.unnamed);\n\t\t\t}\n\t\t}\n\n\t\treturn new WatcherRepoChangeEvent(\n\t\t\tthis.repoPath,\n\t\t\t[...this._changes, ...changes],\n\t\t\t{ names: names, unnamed: unnamed },\n\t\t\tfilter(this._announced, c => !changes.includes(c)),\n\t\t);\n\t}\n}\n\n/**\n * Returns the name in `names` that covers `ref` — `ref` itself, or a ref directory above it (compared on `/`\n * boundaries, so `refs/heads/fe` doesn't cover `refs/heads/feature`) — or `undefined` if none does.\n */\nexport function findCoveringRefName(names: ReadonlySet<string>, ref: string): string | undefined {\n\tif (names.size === 0) return undefined;\n\tif (names.has(ref)) return ref;\n\n\tlet index = ref.lastIndexOf('/');\n\twhile (index > 0) {\n\t\tconst dir = ref.substring(0, index);\n\t\tif (names.has(dir)) return dir;\n\n\t\tindex = ref.lastIndexOf('/', index - 1);\n\t}\n\treturn undefined;\n}\n\n/**\n * An event describing working tree file changes, produced by the working tree pipeline.\n *\n * Paths have already been filtered through the gitignore filter and noise filter.\n */\nexport interface WorkingTreeChangeEvent {\n\treadonly repoPath: string;\n\t/**\n\t * Absolute paths of changed files or directories (post-gitignore filtering). A directory stands for everything\n\t * beneath it. Partial when {@link incomplete}.\n\t */\n\treadonly paths: ReadonlySet<string>;\n\t/**\n\t * `true` when more paths changed than the accumulator retains, so {@link paths} is a partial set and a\n\t * miss does NOT mean the file is unchanged. Consumers testing membership must treat this as a match.\n\t */\n\treadonly incomplete: boolean;\n}\n"],"mappings":"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"}
@@ -0,0 +1,111 @@
1
+ import { UnifiedDisposable } from "../../utils/disposable.js";
2
+ import { FileWatchEvent } from "./provider.js";
3
+ import { WatchPollingOptions } from "./watchService.js";
4
+ //#region ../git/src/watching/gitDirPoller.d.ts
5
+ type GitDirStat = Awaited<ReturnType<WatchPollingOptions['stat']>>;
6
+ /** Something the {@link GitDirPoller} reads once per pass */
7
+ export interface PollTarget {
8
+ poll(): Promise<void>;
9
+ }
10
+ /** A file or directory a poll stats, relative to the directory it watches in place of a watcher */
11
+ export interface GitDirProbe {
12
+ /** `/`-separated, relative to the target's base path */
13
+ readonly path: string;
14
+ /** The relative paths a move is reported as, in place of `path` */
15
+ readonly emitAs?: readonly string[];
16
+ }
17
+ /**
18
+ * Runs passes over the poll targets of every watcher that can't be trusted, reading at most 4 targets at once across
19
+ * passes and first readings. A target is read once soon after it is added, so its baseline doesn't wait for the next
20
+ * pass; targets added together are read together.
21
+ *
22
+ * With the default gate (no `isActive`), one timer runs a pass every `intervalMs`, stopped by {@link suspend} and
23
+ * restarted by {@link resume}, which runs a pass at once. With a host's `isActive`, whose changes core can't see, one
24
+ * timer looks at it every `activeCheckMs`: a pass is due once `intervalMs` has gone by since the last one, or at the
25
+ * first look after the gate opens (or after {@link nudge}), so a window that comes back between passes doesn't wait out
26
+ * the rest of one.
27
+ */
28
+ export declare class GitDirPoller implements UnifiedDisposable {
29
+ readonly options: WatchPollingOptions;
30
+ private readonly targets;
31
+ /** Targets added since the last pass and not yet read */
32
+ private readonly unread;
33
+ /** Each target's last read asked for; a target is never read twice at once, so the next waits for it */
34
+ private readonly reading;
35
+ /** Reads waiting for one of the {@link defaultPollConcurrency} slots, which a finished read hands over */
36
+ private readonly waiting;
37
+ /**
38
+ * The stats of the batches (a pass, or first readings) in flight, shared by every target that probes the same path,
39
+ * as the main checkout's root and its common dir do
40
+ */
41
+ private readonly stats;
42
+ private readonly intervalMs;
43
+ private readonly activeCheckMs;
44
+ private timer;
45
+ private running;
46
+ private batches;
47
+ private slotsInUse;
48
+ /** The next look runs a pass whatever the interval says */
49
+ private due;
50
+ private lastPassAt;
51
+ private suspended;
52
+ private disposed;
53
+ constructor(options: WatchPollingOptions);
54
+ [Symbol.dispose](): void;
55
+ dispose(): void;
56
+ get size(): number;
57
+ add(target: PollTarget): void;
58
+ delete(target: PollTarget): void;
59
+ /** Makes the next look a pass, and looks now */
60
+ nudge(): void;
61
+ /** Under the default gate, stops passes (and the timer) until {@link resume}; a host's `isActive` ignores it */
62
+ suspend(): void;
63
+ /** Lifts {@link suspend}, and runs a pass now, so what moved meanwhile is polled rather than at the next look */
64
+ resume(): void;
65
+ /** One look at the gate: runs a pass when one is due, the gate is open and none is in flight */
66
+ tick(): Promise<void>;
67
+ /** Stats `path` once for every target that probes it in the batches in flight */
68
+ stat(path: string): Promise<GitDirStat>;
69
+ /** `path` changed since it was statted, so the next target to probe it in a batch in flight stats it again */
70
+ invalidate(path: string): void;
71
+ private pass;
72
+ private readUnread;
73
+ private readAll;
74
+ /** Reads `target` once any read of it in flight ends, so the reading is fresh and its probes current */
75
+ private read;
76
+ private acquireSlot;
77
+ private releaseSlot;
78
+ private startTimer;
79
+ private stopTimer;
80
+ }
81
+ /**
82
+ * Stands in for a watcher on `basePath`: stats each probe and reports one whose `mtime:size` moved as a `change` of
83
+ * the paths it is emitted as, through the same handler the watcher's own events take. A probe's first read (and a
84
+ * probe new to the set, as after a branch switch) only records; one that was missing and is now present moved. A stat
85
+ * that throws keeps the last reading, so it never counts as a move.
86
+ */
87
+ export declare class GitDirProbeTarget implements PollTarget {
88
+ private readonly basePath;
89
+ private readonly stats;
90
+ private readonly getProbes;
91
+ /** For each path a probe may be emitted as, the paths of the probes emitted as it */
92
+ private readonly probesByEmittedPath;
93
+ private readonly onMoved;
94
+ private readonly signatures;
95
+ /** For each poll whose stats are in flight, the paths {@link forget} cleared meanwhile, which it must not record */
96
+ private readonly forgottenDuringPolls;
97
+ constructor(basePath: string, stats: Pick<GitDirPoller, 'stat' | 'invalidate'>, getProbes: () => Iterable<GitDirProbe>,
98
+ /** For each path a probe may be emitted as, the paths of the probes emitted as it */
99
+ probesByEmittedPath: ReadonlyMap<string, readonly string[]>, onMoved: (event: FileWatchEvent, probe: GitDirProbe) => void);
100
+ poll(): Promise<void>;
101
+ /**
102
+ * The watcher reported `path` itself, so the next poll records the probes it touched (the path, a ref's lock file
103
+ * standing for the ref, the directories above it, and the probes emitted as it, like `logs/HEAD` as `HEAD`, which
104
+ * the watcher never sees) without reporting them again.
105
+ */
106
+ forget(path: string): void;
107
+ private clearSignature;
108
+ private read;
109
+ }
110
+ //#endregion
111
+ //# sourceMappingURL=gitDirPoller.d.ts.map
@@ -0,0 +1,266 @@
1
+ import { isDescendant, joinPaths, relative } from "../../utils/path.js";
2
+ //#region ../git/src/watching/gitDirPoller.ts
3
+ const defaultPollIntervalMs = 6e4;
4
+ const defaultPollActiveCheckMs = 5e3;
5
+ const defaultPollConcurrency = 4;
6
+ /**
7
+ * Runs passes over the poll targets of every watcher that can't be trusted, reading at most 4 targets at once across
8
+ * passes and first readings. A target is read once soon after it is added, so its baseline doesn't wait for the next
9
+ * pass; targets added together are read together.
10
+ *
11
+ * With the default gate (no `isActive`), one timer runs a pass every `intervalMs`, stopped by {@link suspend} and
12
+ * restarted by {@link resume}, which runs a pass at once. With a host's `isActive`, whose changes core can't see, one
13
+ * timer looks at it every `activeCheckMs`: a pass is due once `intervalMs` has gone by since the last one, or at the
14
+ * first look after the gate opens (or after {@link nudge}), so a window that comes back between passes doesn't wait out
15
+ * the rest of one.
16
+ */
17
+ var GitDirPoller = class {
18
+ options;
19
+ targets = /* @__PURE__ */ new Set();
20
+ /** Targets added since the last pass and not yet read */
21
+ unread = /* @__PURE__ */ new Set();
22
+ /** Each target's last read asked for; a target is never read twice at once, so the next waits for it */
23
+ reading = /* @__PURE__ */ new Map();
24
+ /** Reads waiting for one of the {@link defaultPollConcurrency} slots, which a finished read hands over */
25
+ waiting = [];
26
+ /**
27
+ * The stats of the batches (a pass, or first readings) in flight, shared by every target that probes the same path,
28
+ * as the main checkout's root and its common dir do
29
+ */
30
+ stats = /* @__PURE__ */ new Map();
31
+ intervalMs;
32
+ activeCheckMs;
33
+ timer;
34
+ running;
35
+ batches = 0;
36
+ slotsInUse = 0;
37
+ /** The next look runs a pass whatever the interval says */
38
+ due = true;
39
+ lastPassAt = 0;
40
+ suspended = false;
41
+ disposed = false;
42
+ constructor(options) {
43
+ this.options = options;
44
+ this.intervalMs = options.intervalMs ?? defaultPollIntervalMs;
45
+ this.activeCheckMs = options.activeCheckMs ?? defaultPollActiveCheckMs;
46
+ }
47
+ [Symbol.dispose]() {
48
+ this.dispose();
49
+ }
50
+ dispose() {
51
+ this.disposed = true;
52
+ this.targets.clear();
53
+ this.unread.clear();
54
+ this.stats.clear();
55
+ this.stopTimer();
56
+ }
57
+ get size() {
58
+ return this.targets.size;
59
+ }
60
+ add(target) {
61
+ if (this.disposed || this.targets.has(target)) return;
62
+ this.targets.add(target);
63
+ this.unread.add(target);
64
+ if (this.unread.size === 1) queueMicrotask(() => this.readUnread());
65
+ this.startTimer();
66
+ }
67
+ delete(target) {
68
+ this.unread.delete(target);
69
+ if (!this.targets.delete(target) || this.targets.size !== 0) return;
70
+ this.stopTimer();
71
+ }
72
+ /** Makes the next look a pass, and looks now */
73
+ nudge() {
74
+ this.due = true;
75
+ this.tick();
76
+ }
77
+ /** Under the default gate, stops passes (and the timer) until {@link resume}; a host's `isActive` ignores it */
78
+ suspend() {
79
+ this.suspended = true;
80
+ if (this.options.isActive == null) this.stopTimer();
81
+ }
82
+ /** Lifts {@link suspend}, and runs a pass now, so what moved meanwhile is polled rather than at the next look */
83
+ resume() {
84
+ this.suspended = false;
85
+ this.nudge();
86
+ }
87
+ /** One look at the gate: runs a pass when one is due, the gate is open and none is in flight */
88
+ tick() {
89
+ if (this.disposed || this.targets.size === 0) return Promise.resolve();
90
+ if (!(this.options.isActive?.() ?? !this.suspended)) {
91
+ this.due = true;
92
+ return Promise.resolve();
93
+ }
94
+ if (this.running != null) return this.running;
95
+ const now = performance.now();
96
+ if (!this.due && now - this.lastPassAt < this.intervalMs) return Promise.resolve();
97
+ if (this.options.isActive == null) {
98
+ this.stopTimer();
99
+ this.startTimer();
100
+ }
101
+ this.due = false;
102
+ this.lastPassAt = now;
103
+ this.running = this.pass().finally(() => {
104
+ this.running = void 0;
105
+ if (this.due) this.tick();
106
+ });
107
+ return this.running;
108
+ }
109
+ /** Stats `path` once for every target that probes it in the batches in flight */
110
+ stat(path) {
111
+ if (this.batches === 0) return this.options.stat(path);
112
+ let stat = this.stats.get(path);
113
+ if (stat == null) {
114
+ stat = this.options.stat(path);
115
+ this.stats.set(path, stat);
116
+ }
117
+ return stat;
118
+ }
119
+ /** `path` changed since it was statted, so the next target to probe it in a batch in flight stats it again */
120
+ invalidate(path) {
121
+ this.stats.delete(path);
122
+ }
123
+ pass() {
124
+ this.stats.clear();
125
+ this.unread.clear();
126
+ return this.readAll([...this.targets]);
127
+ }
128
+ readUnread() {
129
+ if (this.unread.size === 0) return;
130
+ const targets = [...this.unread];
131
+ this.unread.clear();
132
+ this.readAll(targets);
133
+ }
134
+ async readAll(targets) {
135
+ this.batches++;
136
+ try {
137
+ await Promise.allSettled(targets.map((target) => this.read(target)));
138
+ } finally {
139
+ this.batches--;
140
+ if (this.batches === 0) this.stats.clear();
141
+ }
142
+ }
143
+ /** Reads `target` once any read of it in flight ends, so the reading is fresh and its probes current */
144
+ read(target) {
145
+ const poll = async () => {
146
+ if (!this.targets.has(target)) return;
147
+ await this.acquireSlot();
148
+ try {
149
+ if (this.targets.has(target)) await target.poll();
150
+ } finally {
151
+ this.releaseSlot();
152
+ }
153
+ };
154
+ const previous = this.reading.get(target);
155
+ const reading = (previous != null ? previous.then(poll, poll) : poll()).finally(() => {
156
+ if (this.reading.get(target) === reading) this.reading.delete(target);
157
+ });
158
+ this.reading.set(target, reading);
159
+ return reading;
160
+ }
161
+ acquireSlot() {
162
+ if (this.slotsInUse < defaultPollConcurrency) {
163
+ this.slotsInUse++;
164
+ return;
165
+ }
166
+ return new Promise((resolve) => this.waiting.push(resolve));
167
+ }
168
+ releaseSlot() {
169
+ const next = this.waiting.shift();
170
+ if (next != null) next();
171
+ else this.slotsInUse--;
172
+ }
173
+ startTimer() {
174
+ if (this.timer != null || this.disposed || this.targets.size === 0) return;
175
+ if (this.options.isActive != null) this.timer = setInterval(() => void this.tick(), this.activeCheckMs);
176
+ else if (!this.suspended) this.timer = setTimeout(() => {
177
+ this.timer = void 0;
178
+ this.due = true;
179
+ this.tick();
180
+ }, this.intervalMs);
181
+ else return;
182
+ if (typeof this.timer !== "number") this.timer.unref();
183
+ }
184
+ stopTimer() {
185
+ if (this.timer == null) return;
186
+ if (this.options.isActive != null) clearInterval(this.timer);
187
+ else clearTimeout(this.timer);
188
+ this.timer = void 0;
189
+ this.due = true;
190
+ }
191
+ };
192
+ /**
193
+ * Stands in for a watcher on `basePath`: stats each probe and reports one whose `mtime:size` moved as a `change` of
194
+ * the paths it is emitted as, through the same handler the watcher's own events take. A probe's first read (and a
195
+ * probe new to the set, as after a branch switch) only records; one that was missing and is now present moved. A stat
196
+ * that throws keeps the last reading, so it never counts as a move.
197
+ */
198
+ var GitDirProbeTarget = class {
199
+ basePath;
200
+ stats;
201
+ getProbes;
202
+ probesByEmittedPath;
203
+ onMoved;
204
+ signatures = /* @__PURE__ */ new Map();
205
+ /** For each poll whose stats are in flight, the paths {@link forget} cleared meanwhile, which it must not record */
206
+ forgottenDuringPolls = /* @__PURE__ */ new Set();
207
+ constructor(basePath, stats, getProbes, probesByEmittedPath, onMoved) {
208
+ this.basePath = basePath;
209
+ this.stats = stats;
210
+ this.getProbes = getProbes;
211
+ this.probesByEmittedPath = probesByEmittedPath;
212
+ this.onMoved = onMoved;
213
+ }
214
+ async poll() {
215
+ const probes = [...this.getProbes()];
216
+ const forgotten = /* @__PURE__ */ new Set();
217
+ this.forgottenDuringPolls.add(forgotten);
218
+ const readings = await Promise.all(probes.map((p) => this.read(p.path)));
219
+ this.forgottenDuringPolls.delete(forgotten);
220
+ const seen = /* @__PURE__ */ new Set();
221
+ const moved = [];
222
+ for (let i = 0; i < probes.length; i++) {
223
+ const probe = probes[i];
224
+ seen.add(probe.path);
225
+ const reading = readings[i];
226
+ if (reading == null || forgotten.has(probe.path)) continue;
227
+ const previous = this.signatures.get(probe.path);
228
+ this.signatures.set(probe.path, reading);
229
+ if (previous != null && previous !== reading) moved.push(probe);
230
+ }
231
+ for (const path of this.signatures.keys()) if (!seen.has(path)) this.signatures.delete(path);
232
+ for (const probe of moved) for (const path of probe.emitAs ?? [probe.path]) this.onMoved({
233
+ path: joinPaths(this.basePath, path),
234
+ reason: "change"
235
+ }, probe);
236
+ }
237
+ /**
238
+ * The watcher reported `path` itself, so the next poll records the probes it touched (the path, a ref's lock file
239
+ * standing for the ref, the directories above it, and the probes emitted as it, like `logs/HEAD` as `HEAD`, which
240
+ * the watcher never sees) without reporting them again.
241
+ */
242
+ forget(path) {
243
+ if (!isDescendant(path, this.basePath)) return;
244
+ let relativePath = relative(this.basePath, path);
245
+ if (relativePath.endsWith(".lock")) relativePath = relativePath.substring(0, relativePath.length - 5);
246
+ for (let p = relativePath; p.length > 0; p = p.substring(0, Math.max(0, p.lastIndexOf("/")))) this.clearSignature(p);
247
+ for (const probePath of this.probesByEmittedPath.get(relativePath) ?? []) this.clearSignature(probePath);
248
+ }
249
+ clearSignature(path) {
250
+ this.signatures.delete(path);
251
+ this.stats.invalidate(joinPaths(this.basePath, path));
252
+ for (const forgotten of this.forgottenDuringPolls) forgotten.add(path);
253
+ }
254
+ async read(path) {
255
+ try {
256
+ const stat = await this.stats.stat(joinPaths(this.basePath, path));
257
+ return stat == null ? "missing" : `${stat.mtime}:${stat.size}`;
258
+ } catch {
259
+ return;
260
+ }
261
+ }
262
+ };
263
+ //#endregion
264
+ export { GitDirPoller, GitDirProbeTarget };
265
+
266
+ //# sourceMappingURL=gitDirPoller.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gitDirPoller.js","names":[],"sources":["../../../../git/src/watching/gitDirPoller.ts"],"sourcesContent":["import type { UnifiedDisposable } from '@gitlens/utils/disposable.js';\nimport { isDescendant, joinPaths, relative } from '@gitlens/utils/path.js';\nimport type { FileWatchEvent } from './provider.js';\nimport type { WatchPollingOptions } from './watchService.js';\n\nconst defaultPollIntervalMs = 60_000;\nconst defaultPollActiveCheckMs = 5_000;\nconst defaultPollConcurrency = 4;\n\ntype GitDirStat = Awaited<ReturnType<WatchPollingOptions['stat']>>;\n\n/** Something the {@link GitDirPoller} reads once per pass */\nexport interface PollTarget {\n\tpoll(): Promise<void>;\n}\n\n/** A file or directory a poll stats, relative to the directory it watches in place of a watcher */\nexport interface GitDirProbe {\n\t/** `/`-separated, relative to the target's base path */\n\treadonly path: string;\n\t/** The relative paths a move is reported as, in place of `path` */\n\treadonly emitAs?: readonly string[];\n}\n\n/**\n * Runs passes over the poll targets of every watcher that can't be trusted, reading at most 4 targets at once across\n * passes and first readings. A target is read once soon after it is added, so its baseline doesn't wait for the next\n * pass; targets added together are read together.\n *\n * With the default gate (no `isActive`), one timer runs a pass every `intervalMs`, stopped by {@link suspend} and\n * restarted by {@link resume}, which runs a pass at once. With a host's `isActive`, whose changes core can't see, one\n * timer looks at it every `activeCheckMs`: a pass is due once `intervalMs` has gone by since the last one, or at the\n * first look after the gate opens (or after {@link nudge}), so a window that comes back between passes doesn't wait out\n * the rest of one.\n */\nexport class GitDirPoller implements UnifiedDisposable {\n\tprivate readonly targets = new Set<PollTarget>();\n\t/** Targets added since the last pass and not yet read */\n\tprivate readonly unread = new Set<PollTarget>();\n\t/** Each target's last read asked for; a target is never read twice at once, so the next waits for it */\n\tprivate readonly reading = new Map<PollTarget, Promise<void>>();\n\t/** Reads waiting for one of the {@link defaultPollConcurrency} slots, which a finished read hands over */\n\tprivate readonly waiting: (() => void)[] = [];\n\t/**\n\t * The stats of the batches (a pass, or first readings) in flight, shared by every target that probes the same path,\n\t * as the main checkout's root and its common dir do\n\t */\n\tprivate readonly stats = new Map<string, Promise<GitDirStat>>();\n\tprivate readonly intervalMs: number;\n\tprivate readonly activeCheckMs: number;\n\tprivate timer: ReturnType<typeof setTimeout> | undefined;\n\tprivate running: Promise<void> | undefined;\n\tprivate batches = 0;\n\tprivate slotsInUse = 0;\n\t/** The next look runs a pass whatever the interval says */\n\tprivate due = true;\n\tprivate lastPassAt = 0;\n\tprivate suspended = false;\n\tprivate disposed = false;\n\n\tconstructor(readonly options: WatchPollingOptions) {\n\t\tthis.intervalMs = options.intervalMs ?? defaultPollIntervalMs;\n\t\tthis.activeCheckMs = options.activeCheckMs ?? defaultPollActiveCheckMs;\n\t}\n\n\t[Symbol.dispose](): void {\n\t\tthis.dispose();\n\t}\n\n\tdispose(): void {\n\t\tthis.disposed = true;\n\t\tthis.targets.clear();\n\t\tthis.unread.clear();\n\t\tthis.stats.clear();\n\t\tthis.stopTimer();\n\t}\n\n\tget size(): number {\n\t\treturn this.targets.size;\n\t}\n\n\tadd(target: PollTarget): void {\n\t\tif (this.disposed || this.targets.has(target)) return;\n\n\t\tthis.targets.add(target);\n\t\tthis.unread.add(target);\n\t\tif (this.unread.size === 1) {\n\t\t\tqueueMicrotask(() => this.readUnread());\n\t\t}\n\t\tthis.startTimer();\n\t}\n\n\tdelete(target: PollTarget): void {\n\t\tthis.unread.delete(target);\n\t\tif (!this.targets.delete(target) || this.targets.size !== 0) return;\n\n\t\tthis.stopTimer();\n\t}\n\n\t/** Makes the next look a pass, and looks now */\n\tnudge(): void {\n\t\tthis.due = true;\n\t\tvoid this.tick();\n\t}\n\n\t/** Under the default gate, stops passes (and the timer) until {@link resume}; a host's `isActive` ignores it */\n\tsuspend(): void {\n\t\tthis.suspended = true;\n\t\tif (this.options.isActive == null) {\n\t\t\tthis.stopTimer();\n\t\t}\n\t}\n\n\t/** Lifts {@link suspend}, and runs a pass now, so what moved meanwhile is polled rather than at the next look */\n\tresume(): void {\n\t\tthis.suspended = false;\n\t\tthis.nudge();\n\t}\n\n\t/** One look at the gate: runs a pass when one is due, the gate is open and none is in flight */\n\ttick(): Promise<void> {\n\t\tif (this.disposed || this.targets.size === 0) return Promise.resolve();\n\n\t\tif (!(this.options.isActive?.() ?? !this.suspended)) {\n\t\t\tthis.due = true;\n\t\t\treturn Promise.resolve();\n\t\t}\n\n\t\t// A pass asked for while one is in flight runs once it ends; an elapsed interval just waits for the next look\n\t\tif (this.running != null) return this.running;\n\n\t\tconst now = performance.now();\n\t\tif (!this.due && now - this.lastPassAt < this.intervalMs) return Promise.resolve();\n\n\t\t// Under the default gate the timer counts the interval from this pass, whatever started it\n\t\tif (this.options.isActive == null) {\n\t\t\tthis.stopTimer();\n\t\t\tthis.startTimer();\n\t\t}\n\t\tthis.due = false;\n\t\tthis.lastPassAt = now;\n\t\tthis.running = this.pass().finally(() => {\n\t\t\tthis.running = undefined;\n\t\t\tif (this.due) {\n\t\t\t\tvoid this.tick();\n\t\t\t}\n\t\t});\n\t\treturn this.running;\n\t}\n\n\t/** Stats `path` once for every target that probes it in the batches in flight */\n\tstat(path: string): Promise<GitDirStat> {\n\t\tif (this.batches === 0) return this.options.stat(path);\n\n\t\tlet stat = this.stats.get(path);\n\t\tif (stat == null) {\n\t\t\tstat = this.options.stat(path);\n\t\t\tthis.stats.set(path, stat);\n\t\t}\n\t\treturn stat;\n\t}\n\n\t/** `path` changed since it was statted, so the next target to probe it in a batch in flight stats it again */\n\tinvalidate(path: string): void {\n\t\tthis.stats.delete(path);\n\t}\n\n\tprivate pass(): Promise<void> {\n\t\t// A pass reads its own stats, never ones from before it began\n\t\tthis.stats.clear();\n\t\t// The pass baselines any target not read yet\n\t\tthis.unread.clear();\n\t\treturn this.readAll([...this.targets]);\n\t}\n\n\tprivate readUnread(): void {\n\t\tif (this.unread.size === 0) return;\n\n\t\tconst targets = [...this.unread];\n\t\tthis.unread.clear();\n\t\tvoid this.readAll(targets);\n\t}\n\n\tprivate async readAll(targets: readonly PollTarget[]): Promise<void> {\n\t\tthis.batches++;\n\t\ttry {\n\t\t\t// A target that fails is read again next pass; nothing it saw this time is reported\n\t\t\tawait Promise.allSettled(targets.map(target => this.read(target)));\n\t\t} finally {\n\t\t\tthis.batches--;\n\t\t\tif (this.batches === 0) {\n\t\t\t\tthis.stats.clear();\n\t\t\t}\n\t\t}\n\t}\n\n\t/** Reads `target` once any read of it in flight ends, so the reading is fresh and its probes current */\n\tprivate read(target: PollTarget): Promise<void> {\n\t\tconst poll = async (): Promise<void> => {\n\t\t\tif (!this.targets.has(target)) return;\n\n\t\t\tawait this.acquireSlot();\n\t\t\ttry {\n\t\t\t\tif (this.targets.has(target)) {\n\t\t\t\t\tawait target.poll();\n\t\t\t\t}\n\t\t\t} finally {\n\t\t\t\tthis.releaseSlot();\n\t\t\t}\n\t\t};\n\t\tconst previous = this.reading.get(target);\n\t\tconst reading: Promise<void> = (previous != null ? previous.then(poll, poll) : poll()).finally(() => {\n\t\t\tif (this.reading.get(target) === reading) {\n\t\t\t\tthis.reading.delete(target);\n\t\t\t}\n\t\t});\n\t\tthis.reading.set(target, reading);\n\t\treturn reading;\n\t}\n\n\tprivate acquireSlot(): Promise<void> | undefined {\n\t\tif (this.slotsInUse < defaultPollConcurrency) {\n\t\t\tthis.slotsInUse++;\n\t\t\treturn undefined;\n\t\t}\n\n\t\treturn new Promise(resolve => this.waiting.push(resolve));\n\t}\n\n\tprivate releaseSlot(): void {\n\t\tconst next = this.waiting.shift();\n\t\tif (next != null) {\n\t\t\tnext();\n\t\t} else {\n\t\t\tthis.slotsInUse--;\n\t\t}\n\t}\n\n\tprivate startTimer(): void {\n\t\tif (this.timer != null || this.disposed || this.targets.size === 0) return;\n\n\t\tif (this.options.isActive != null) {\n\t\t\tthis.timer = setInterval(() => void this.tick(), this.activeCheckMs);\n\t\t} else if (!this.suspended) {\n\t\t\tthis.timer = setTimeout(() => {\n\t\t\t\tthis.timer = undefined;\n\t\t\t\tthis.due = true;\n\t\t\t\tvoid this.tick();\n\t\t\t}, this.intervalMs);\n\t\t} else {\n\t\t\treturn;\n\t\t}\n\n\t\t// A poll alone must not keep a (Node) process alive; in the browser timers are numbers\n\t\tif (typeof this.timer !== 'number') {\n\t\t\tthis.timer.unref();\n\t\t}\n\t}\n\n\tprivate stopTimer(): void {\n\t\tif (this.timer == null) return;\n\n\t\tif (this.options.isActive != null) {\n\t\t\tclearInterval(this.timer);\n\t\t} else {\n\t\t\tclearTimeout(this.timer);\n\t\t}\n\t\tthis.timer = undefined;\n\t\tthis.due = true;\n\t}\n}\n\n/**\n * Stands in for a watcher on `basePath`: stats each probe and reports one whose `mtime:size` moved as a `change` of\n * the paths it is emitted as, through the same handler the watcher's own events take. A probe's first read (and a\n * probe new to the set, as after a branch switch) only records; one that was missing and is now present moved. A stat\n * that throws keeps the last reading, so it never counts as a move.\n */\nexport class GitDirProbeTarget implements PollTarget {\n\tprivate readonly signatures = new Map<string, string>();\n\t/** For each poll whose stats are in flight, the paths {@link forget} cleared meanwhile, which it must not record */\n\tprivate readonly forgottenDuringPolls = new Set<Set<string>>();\n\n\tconstructor(\n\t\tprivate readonly basePath: string,\n\t\tprivate readonly stats: Pick<GitDirPoller, 'stat' | 'invalidate'>,\n\t\tprivate readonly getProbes: () => Iterable<GitDirProbe>,\n\t\t/** For each path a probe may be emitted as, the paths of the probes emitted as it */\n\t\tprivate readonly probesByEmittedPath: ReadonlyMap<string, readonly string[]>,\n\t\tprivate readonly onMoved: (event: FileWatchEvent, probe: GitDirProbe) => void,\n\t) {}\n\n\tasync poll(): Promise<void> {\n\t\tconst probes = [...this.getProbes()];\n\t\tconst forgotten = new Set<string>();\n\t\tthis.forgottenDuringPolls.add(forgotten);\n\t\tconst readings = await Promise.all(probes.map(p => this.read(p.path)));\n\t\tthis.forgottenDuringPolls.delete(forgotten);\n\n\t\tconst seen = new Set<string>();\n\t\tconst moved: GitDirProbe[] = [];\n\t\tfor (let i = 0; i < probes.length; i++) {\n\t\t\tconst probe = probes[i];\n\t\t\tseen.add(probe.path);\n\n\t\t\tconst reading = readings[i];\n\t\t\tif (reading == null || forgotten.has(probe.path)) continue;\n\n\t\t\tconst previous = this.signatures.get(probe.path);\n\t\t\tthis.signatures.set(probe.path, reading);\n\t\t\tif (previous != null && previous !== reading) {\n\t\t\t\tmoved.push(probe);\n\t\t\t}\n\t\t}\n\n\t\t// A probe that left the set starts over if it comes back\n\t\tfor (const path of this.signatures.keys()) {\n\t\t\tif (!seen.has(path)) {\n\t\t\t\tthis.signatures.delete(path);\n\t\t\t}\n\t\t}\n\n\t\tfor (const probe of moved) {\n\t\t\tfor (const path of probe.emitAs ?? [probe.path]) {\n\t\t\t\tthis.onMoved({ path: joinPaths(this.basePath, path), reason: 'change' }, probe);\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * The watcher reported `path` itself, so the next poll records the probes it touched (the path, a ref's lock file\n\t * standing for the ref, the directories above it, and the probes emitted as it, like `logs/HEAD` as `HEAD`, which\n\t * the watcher never sees) without reporting them again.\n\t */\n\tforget(path: string): void {\n\t\tif (!isDescendant(path, this.basePath)) return;\n\n\t\tlet relativePath = relative(this.basePath, path);\n\t\tif (relativePath.endsWith('.lock')) {\n\t\t\trelativePath = relativePath.substring(0, relativePath.length - 5);\n\t\t}\n\n\t\tfor (let p = relativePath; p.length > 0; p = p.substring(0, Math.max(0, p.lastIndexOf('/')))) {\n\t\t\tthis.clearSignature(p);\n\t\t}\n\n\t\tfor (const probePath of this.probesByEmittedPath.get(relativePath) ?? []) {\n\t\t\tthis.clearSignature(probePath);\n\t\t}\n\t}\n\n\tprivate clearSignature(path: string): void {\n\t\tthis.signatures.delete(path);\n\t\tthis.stats.invalidate(joinPaths(this.basePath, path));\n\t\tfor (const forgotten of this.forgottenDuringPolls) {\n\t\t\tforgotten.add(path);\n\t\t}\n\t}\n\n\tprivate async read(path: string): Promise<string | undefined> {\n\t\ttry {\n\t\t\tconst stat = await this.stats.stat(joinPaths(this.basePath, path));\n\t\t\treturn stat == null ? 'missing' : `${stat.mtime}:${stat.size}`;\n\t\t} catch {\n\t\t\treturn undefined;\n\t\t}\n\t}\n}\n"],"mappings":"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"}
@@ -17,6 +17,15 @@ export interface FileWatchEvent {
17
17
  }
18
18
  /** Opaque token representing a filesystem watcher */
19
19
  export interface FileWatcher extends UnifiedDisposable {}
20
+ /**
21
+ * How much of what changes under a watcher's `basePath` it reports:
22
+ * - `reliable`: everything (the state of a watcher that never reports one);
23
+ * - `unreliable`: what changes on this machine, but maybe not another's edits (a network volume), so core polls
24
+ * beside it;
25
+ * - `unwatchable`: nothing (a filesystem without change notifications, or a watch that went down), so core polls
26
+ * instead.
27
+ */
28
+ export type FileWatcherReliability = 'reliable' | 'unreliable' | 'unwatchable';
20
29
  export interface FileWatcherOptions {
21
30
  /**
22
31
  * Directories to leave unwatched, each with everything beneath it. Entries are `/`-separated and relative to
@@ -26,6 +35,13 @@ export interface FileWatcherOptions {
26
35
  * (Map each entry to your backend's syntax — e.g. picomatch-based watchers need both `X` and `X/**`.)
27
36
  */
28
37
  readonly excludes?: readonly string[];
38
+ /**
39
+ * Reports this watcher's {@link FileWatcherReliability} whenever it changes; passed only when the host opted into
40
+ * polling (`WatchServiceOptions.polling`). May be called synchronously inside `createWatcher`, and again later
41
+ * (a watch going down, or coming back). Each watcher reports its own state, so one created on a path that is
42
+ * already down reports `unwatchable` itself. Going from `unwatchable` to anything else makes core rescan.
43
+ */
44
+ readonly onDidChangeReliability?: (state: FileWatcherReliability) => void;
29
45
  }
30
46
  /**
31
47
  * Factory for creating filesystem watchers. The library calls this