@gitkraken/core-gitlens 0.16.1 → 0.16.3

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 (36) hide show
  1. package/CHANGELOG.md +16 -1
  2. package/dist/git/watching/classifyChange.js +5 -2
  3. package/dist/git/watching/classifyChange.js.map +1 -1
  4. package/dist/git/watching/watchGroup.js +2 -1
  5. package/dist/git/watching/watchGroup.js.map +1 -1
  6. package/dist/git-cli/providers/branches.d.ts +33 -9
  7. package/dist/git-cli/providers/branches.js +124 -41
  8. package/dist/git-cli/providers/branches.js.map +1 -1
  9. package/dist/plus/integrations/integrationService.d.ts +1 -0
  10. package/dist/plus/integrations/integrationService.js +18 -5
  11. package/dist/plus/integrations/integrationService.js.map +1 -1
  12. package/dist/plus/integrations/providers/apiClients.d.ts +2 -0
  13. package/dist/plus/integrations/providers/apiClients.js +12 -3
  14. package/dist/plus/integrations/providers/apiClients.js.map +1 -1
  15. package/dist/plus/integrations/providers/azure/azure.d.ts +13 -11
  16. package/dist/plus/integrations/providers/azure/azure.js +128 -47
  17. package/dist/plus/integrations/providers/azure/azure.js.map +1 -1
  18. package/dist/plus/integrations/providers/azure/models.d.ts +4 -10
  19. package/dist/plus/integrations/providers/azure/models.js +25 -9
  20. package/dist/plus/integrations/providers/azure/models.js.map +1 -1
  21. package/dist/plus/integrations/providers/azure/workItemDescriptions.d.ts +10 -0
  22. package/dist/plus/integrations/providers/azure/workItemDescriptions.js +48 -0
  23. package/dist/plus/integrations/providers/azure/workItemDescriptions.js.map +1 -0
  24. package/dist/plus/integrations/providers/azure/workItemStateCategories.d.ts +20 -19
  25. package/dist/plus/integrations/providers/azure/workItemStateCategories.js +65 -40
  26. package/dist/plus/integrations/providers/azure/workItemStateCategories.js.map +1 -1
  27. package/dist/plus/integrations/providers/issueSorts.d.ts +0 -1
  28. package/dist/plus/integrations/providers/providerApiClient.d.ts +9 -0
  29. package/dist/plus/integrations/providers/providerApiClient.js +66 -0
  30. package/dist/plus/integrations/providers/providerApiClient.js.map +1 -0
  31. package/dist/plus/integrations/providers/providersApi.d.ts +10 -9
  32. package/dist/plus/integrations/providers/providersApi.js +163 -145
  33. package/dist/plus/integrations/providers/providersApi.js.map +1 -1
  34. package/dist/plus/integrations/reads/drains.d.ts +0 -1
  35. package/docs/integrations.md +30 -13
  36. package/package.json +2 -2
package/CHANGELOG.md CHANGED
@@ -6,6 +6,19 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.16.3] - 2026-10-23
10
+
11
+ ### Fixed
12
+
13
+ - Fixes a fetch that moves nothing reporting `refs/remotes/<remote>/HEAD` as changed, which refreshed every worktree whose base follows it: since git 2.48 every fetch locks that ref to check the remote's default branch, and a ref's lock file counted as a change of the ref. A branch, remote-tracking, tag or replace ref's lock no longer counts; its rename over the ref, or a `packed-refs` rewrite, does. The stash's lock still counts, since `git reflog expire` (as `gc` runs it) and `git reflog delete` rewrite its entries under it without touching the ref (git)
14
+ - Fixes a fetch that moves a worktree's base still costing every worktree based on it a `rev-list --count` of its own to re-measure the base: with git 2.41 or later, each now finds its HEAD's count in one `for-each-ref` shared by every worktree the move reached, counting each local branch a worktree has checked out past the new tip by commit, so a detached HEAD at such a branch's commit and branches at one commit find theirs too, and an unrelated old or orphan branch (`gh-pages`) can't make the shared walk deeper than any worktree's own count. A count that differs from the previous one reads the base in full as before, with no process of the worktree's own, and an equal one is kept without one when the base's reflog reaches nothing off the new tip's history and the fork point is the old or new tip or a commit the reflog holds. Otherwise (another fork point, such as a merge base or one whose reflog entry expired, commits off the new tip's history, a HEAD at a commit no checked-out branch points at, or older git) the worktree runs its own `rev-list --count` as before. The shared read also asks for a commit-graph write, which speeds up its walk over every branch (git-cli)
15
+
16
+ ## [0.16.2] - 2026-10-23
17
+
18
+ ### Fixed
19
+
20
+ - Fixes REST version and work item route failures on TFS 2018 for native issue, pull request and reviewer-group reads. Work item categories use explicit server metadata; unavailable categories keep list results partial and exact state-filtered searches unsupported ([#6048](https://github.com/gitkraken/vscode-gitlens/issues/6048)).
21
+
9
22
  ## [0.16.1] - 2026-10-23
10
23
 
11
24
  ### Added
@@ -884,7 +897,9 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p
884
897
 
885
898
  - Initial release. Bundles `@gitlens/utils`, `@gitlens/git`, `@gitlens/git-cli`, `@gitlens/ai`, and `@gitlens/git-github` into a single core npm package with subpath exports.
886
899
 
887
- [unreleased]: https://github.com/gitkraken/vscode-gitlens/compare/releases/core/v0.16.1...HEAD
900
+ [unreleased]: https://github.com/gitkraken/vscode-gitlens/compare/releases/core/v0.16.3...HEAD
901
+ [0.16.3]: https://github.com/gitkraken/vscode-gitlens/compare/releases/core/v0.16.2...gitkraken:releases/core/v0.16.3
902
+ [0.16.2]: https://github.com/gitkraken/vscode-gitlens/compare/releases/core/v0.16.1...gitkraken:releases/core/v0.16.2
888
903
  [0.16.1]: https://github.com/gitkraken/vscode-gitlens/compare/releases/core/v0.16.0...gitkraken:releases/core/v0.16.1
889
904
  [0.16.0]: https://github.com/gitkraken/vscode-gitlens/compare/releases/core/v0.15.7...gitkraken:releases/core/v0.16.0
890
905
  [0.15.7]: https://github.com/gitkraken/vscode-gitlens/compare/releases/core/v0.15.6...gitkraken:releases/core/v0.15.7
@@ -22,7 +22,7 @@ const allWatchedRepositoryChanges = [
22
22
  "gkConfig"
23
23
  ];
24
24
  const classifyRegex = /(index|HEAD|FETCH_HEAD|ORIG_HEAD|CHERRY_PICK_HEAD|MERGE_HEAD|REBASE_HEAD|rebase-merge|rebase-apply|sequencer|REVERT_HEAD|config|gk\/config|info\/exclude|refs\/(?:heads|remotes|stash|tags|replace)|packed-refs|shallow)/;
25
- /** A ref, ref directory or ref lock in a namespace {@link classifyRegex} maps (`refs/stash.lock` included) */
25
+ /** A ref, ref directory or ref lock in a namespace {@link classifyRegex} maps; of the locks, only the stash's counts */
26
26
  const watchedRefRegex = /^refs\/(?:heads|remotes|stash|tags|replace)(?:\/|\.lock$|$)/;
27
27
  /**
28
28
  * Maps a path relative to a `.git` directory to the corresponding
@@ -53,7 +53,10 @@ function classifyGitDirChange(relativePath) {
53
53
  return file === "HEAD" || file === "locked" || file === "gitdir" ? ["worktrees"] : void 0;
54
54
  }
55
55
  }
56
- if (relativePath.startsWith("refs/") && !watchedRefRegex.test(relativePath)) return void 0;
56
+ if (relativePath.startsWith("refs/")) {
57
+ if (!watchedRefRegex.test(relativePath)) return void 0;
58
+ if (relativePath.endsWith(".lock") && relativePath !== "refs/stash.lock") return void 0;
59
+ }
57
60
  const match = classifyRegex.exec(relativePath);
58
61
  if (match == null) return void 0;
59
62
  switch (match[1]) {
@@ -1 +1 @@
1
- {"version":3,"file":"classifyChange.js","names":[],"sources":["../../../../git/src/watching/classifyChange.ts"],"sourcesContent":["import type { RepositoryChange } from '../models/repository.js';\n\n/**\n * Everything a `.git` watcher can report. Used when events were lost (`overflow`), so every cache type those\n * kinds clear is refreshed. Deliberately not `'unknown'`: `Cache.onRepositoryChanged` treats that as a close and\n * unregisters the path, after which shared evictions stop reaching it.\n */\nexport const allWatchedRepositoryChanges: readonly RepositoryChange[] = [\n\t'index',\n\t'head',\n\t'heads',\n\t'tags',\n\t'stash',\n\t'remotes',\n\t'worktrees',\n\t'config',\n\t'cherryPick',\n\t'merge',\n\t'rebase',\n\t'revert',\n\t'pausedOp',\n\t'ignores',\n\t'gkConfig',\n];\n\nconst classifyRegex =\n\t/(index|HEAD|FETCH_HEAD|ORIG_HEAD|CHERRY_PICK_HEAD|MERGE_HEAD|REBASE_HEAD|rebase-merge|rebase-apply|sequencer|REVERT_HEAD|config|gk\\/config|info\\/exclude|refs\\/(?:heads|remotes|stash|tags|replace)|packed-refs|shallow)/;\n/** A ref, ref directory or ref lock in a namespace {@link classifyRegex} maps (`refs/stash.lock` included) */\nconst watchedRefRegex = /^refs\\/(?:heads|remotes|stash|tags|replace)(?:\\/|\\.lock$|$)/;\n\n/**\n * Maps a path relative to a `.git` directory to the corresponding\n * {@link RepositoryChange} types. This is pure Git knowledge — the\n * regex and switch table encode which files in `.git/` correspond\n * to which logical repository changes.\n *\n * `worktrees` means the worktree list, or what `git worktree list` reports per worktree, changed:\n * `worktrees`, `worktrees/<n>` (add/remove/prune) and `worktrees/<n>/{HEAD,locked,gitdir}`. Any other\n * path under `worktrees/<n>/` (index, `*_HEAD`, `MERGE_*`, rebase state, logs, …) is that worktree's\n * own state, which its own gitdir watcher classifies, so it yields `undefined` here.\n *\n * @param relativePath - Path relative to the common `.git` directory or a worktree's gitdir\n * (e.g., `refs/heads/main`, `HEAD`, `config`, `worktrees/foo/HEAD`)\n * @returns An array of change types, or `undefined` if the path\n * is noise or unrecognized. `undefined` for `FETCH_HEAD` which\n * is intentionally not mapped to a change type (the extension\n * handles it as a last-fetched timestamp).\n */\nexport function classifyGitDirChange(relativePath: string): RepositoryChange[] | undefined {\n\t// User-edited todo file (and its backup) — handled by VS Code's TextDocument\n\t// events in the rebase webview (see `rebaseWebviewProvider.ts`). Suppress here\n\t// to avoid thrashing the repo update path on every save during an interactive\n\t// rebase; rebase lifecycle (start/stop/step) still fires from sibling files\n\t// in `rebase-merge/` and the directory itself.\n\tif (relativePath.endsWith('/git-rebase-todo') || relativePath.endsWith('/git-rebase-todo.backup')) {\n\t\treturn undefined;\n\t}\n\n\tif (relativePath.startsWith('worktrees')) {\n\t\tif (relativePath.length === 9) return ['worktrees'];\n\n\t\tif (relativePath.charCodeAt(9) === 47 /* / */) {\n\t\t\tconst idEnd = relativePath.indexOf('/', 10);\n\t\t\tif (idEnd === -1) return relativePath.length > 10 ? ['worktrees'] : undefined;\n\n\t\t\tconst file = relativePath.substring(idEnd + 1);\n\t\t\treturn file === 'HEAD' || file === 'locked' || file === 'gitdir' ? ['worktrees'] : undefined;\n\t\t}\n\t}\n\n\t// A ref outside the namespaces core reads (a tool's own, such as `refs/undo/*`) is no change, whatever words its\n\t// name holds: `refs/kepler/x/fix-index` must not read as the index\n\tif (relativePath.startsWith('refs/') && !watchedRefRegex.test(relativePath)) return undefined;\n\n\tconst match = classifyRegex.exec(relativePath);\n\tif (match == null) return undefined;\n\n\tswitch (match[1]) {\n\t\tcase 'config':\n\t\t\treturn ['config', 'remotes'];\n\n\t\tcase 'gk/config':\n\t\t\treturn ['gkConfig'];\n\n\t\tcase 'info/exclude':\n\t\t\treturn ['ignores'];\n\n\t\tcase 'index':\n\t\t\treturn ['index'];\n\n\t\tcase 'FETCH_HEAD':\n\t\t\t// No RepositoryChange — extension handles this for last-fetched timestamps\n\t\t\treturn undefined;\n\n\t\tcase 'HEAD':\n\t\t\treturn ['head', 'heads'];\n\n\t\tcase 'ORIG_HEAD':\n\t\t\treturn ['heads'];\n\n\t\tcase 'shallow':\n\t\tcase 'refs/replace':\n\t\t\t// Ancestry changes affect graph, blame/history, health, and ahead/behind consumers even\n\t\t\t// when tips stay put. Reuse their shared history invalidation rather than only refreshing Graph.\n\t\t\treturn ['heads'];\n\n\t\tcase 'CHERRY_PICK_HEAD':\n\t\t\treturn ['cherryPick', 'pausedOp'];\n\n\t\tcase 'MERGE_HEAD':\n\t\t\treturn ['merge', 'pausedOp'];\n\n\t\tcase 'REBASE_HEAD':\n\t\tcase 'rebase-merge':\n\t\tcase 'rebase-apply':\n\t\t\treturn ['rebase', 'pausedOp'];\n\n\t\tcase 'REVERT_HEAD':\n\t\t\treturn ['revert', 'pausedOp'];\n\n\t\tcase 'sequencer':\n\t\t\treturn ['pausedOp'];\n\n\t\t// `'heads'` is deliberately the ONLY thing a `refs/heads/*` event yields, including a create. It is\n\t\t// tempting to read a create as \"a new branch exists under this name\" and use it to drop anything\n\t\t// derived from the old branch's identity (its base, its merge target). That signal does not mean\n\t\t// what it looks like: git rewrites an EXISTING loose ref by writing `<name>.lock` and renaming it\n\t\t// over the file, which watchers report as a create of the destination — so every ordinary commit\n\t\t// would look like a new branch and discard a live branch's metadata. Nothing here can distinguish\n\t\t// the two; identity changes are handled where GitLens performs the operation itself.\n\t\tcase 'refs/heads':\n\t\t\treturn ['heads'];\n\n\t\tcase 'refs/remotes':\n\t\t\treturn ['remotes'];\n\n\t\tcase 'refs/stash':\n\t\t\treturn ['stash'];\n\n\t\tcase 'refs/tags':\n\t\t\treturn ['tags'];\n\n\t\t// A `packed-refs` rewrite (after `git pack-refs` or `git gc`) changes ref state\n\t\t// WITHOUT any `refs/**` file event — deleting a packed branch, for example, only\n\t\t// rewrites this file. The file content is not inspected here, so classification\n\t\t// is deliberately broad: it can add, remove, or move refs of any of these types.\n\t\tcase 'packed-refs':\n\t\t\treturn ['heads', 'remotes', 'tags'];\n\n\t\tdefault:\n\t\t\treturn undefined;\n\t}\n}\n"],"mappings":";;;;;;AAOA,MAAa,8BAA2D;CACvE;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACD;AAEA,MAAM,gBACL;;AAED,MAAM,kBAAkB;;;;;;;;;;;;;;;;;;;AAoBxB,SAAgB,qBAAqB,cAAsD;CAM1F,IAAI,aAAa,SAAS,kBAAkB,KAAK,aAAa,SAAS,yBAAyB,GAC/F;CAGD,IAAI,aAAa,WAAW,WAAW,GAAG;EACzC,IAAI,aAAa,WAAW,GAAG,OAAO,CAAC,WAAW;EAElD,IAAI,aAAa,WAAW,CAAC,MAAM,IAAY;GAC9C,MAAM,QAAQ,aAAa,QAAQ,KAAK,EAAE;GAC1C,IAAI,UAAU,IAAI,OAAO,aAAa,SAAS,KAAK,CAAC,WAAW,IAAI,KAAA;GAEpE,MAAM,OAAO,aAAa,UAAU,QAAQ,CAAC;GAC7C,OAAO,SAAS,UAAU,SAAS,YAAY,SAAS,WAAW,CAAC,WAAW,IAAI,KAAA;EACpF;CACD;CAIA,IAAI,aAAa,WAAW,OAAO,KAAK,CAAC,gBAAgB,KAAK,YAAY,GAAG,OAAO,KAAA;CAEpF,MAAM,QAAQ,cAAc,KAAK,YAAY;CAC7C,IAAI,SAAS,MAAM,OAAO,KAAA;CAE1B,QAAQ,MAAM,IAAd;EACC,KAAK,UACJ,OAAO,CAAC,UAAU,SAAS;EAE5B,KAAK,aACJ,OAAO,CAAC,UAAU;EAEnB,KAAK,gBACJ,OAAO,CAAC,SAAS;EAElB,KAAK,SACJ,OAAO,CAAC,OAAO;EAEhB,KAAK,cAEJ;EAED,KAAK,QACJ,OAAO,CAAC,QAAQ,OAAO;EAExB,KAAK,aACJ,OAAO,CAAC,OAAO;EAEhB,KAAK;EACL,KAAK,gBAGJ,OAAO,CAAC,OAAO;EAEhB,KAAK,oBACJ,OAAO,CAAC,cAAc,UAAU;EAEjC,KAAK,cACJ,OAAO,CAAC,SAAS,UAAU;EAE5B,KAAK;EACL,KAAK;EACL,KAAK,gBACJ,OAAO,CAAC,UAAU,UAAU;EAE7B,KAAK,eACJ,OAAO,CAAC,UAAU,UAAU;EAE7B,KAAK,aACJ,OAAO,CAAC,UAAU;EASnB,KAAK,cACJ,OAAO,CAAC,OAAO;EAEhB,KAAK,gBACJ,OAAO,CAAC,SAAS;EAElB,KAAK,cACJ,OAAO,CAAC,OAAO;EAEhB,KAAK,aACJ,OAAO,CAAC,MAAM;EAMf,KAAK,eACJ,OAAO;GAAC;GAAS;GAAW;EAAM;EAEnC,SACC;CACF;AACD"}
1
+ {"version":3,"file":"classifyChange.js","names":[],"sources":["../../../../git/src/watching/classifyChange.ts"],"sourcesContent":["import type { RepositoryChange } from '../models/repository.js';\n\n/**\n * Everything a `.git` watcher can report. Used when events were lost (`overflow`), so every cache type those\n * kinds clear is refreshed. Deliberately not `'unknown'`: `Cache.onRepositoryChanged` treats that as a close and\n * unregisters the path, after which shared evictions stop reaching it.\n */\nexport const allWatchedRepositoryChanges: readonly RepositoryChange[] = [\n\t'index',\n\t'head',\n\t'heads',\n\t'tags',\n\t'stash',\n\t'remotes',\n\t'worktrees',\n\t'config',\n\t'cherryPick',\n\t'merge',\n\t'rebase',\n\t'revert',\n\t'pausedOp',\n\t'ignores',\n\t'gkConfig',\n];\n\nconst classifyRegex =\n\t/(index|HEAD|FETCH_HEAD|ORIG_HEAD|CHERRY_PICK_HEAD|MERGE_HEAD|REBASE_HEAD|rebase-merge|rebase-apply|sequencer|REVERT_HEAD|config|gk\\/config|info\\/exclude|refs\\/(?:heads|remotes|stash|tags|replace)|packed-refs|shallow)/;\n/** A ref, ref directory or ref lock in a namespace {@link classifyRegex} maps; of the locks, only the stash's counts */\nconst watchedRefRegex = /^refs\\/(?:heads|remotes|stash|tags|replace)(?:\\/|\\.lock$|$)/;\n\n/**\n * Maps a path relative to a `.git` directory to the corresponding\n * {@link RepositoryChange} types. This is pure Git knowledge — the\n * regex and switch table encode which files in `.git/` correspond\n * to which logical repository changes.\n *\n * `worktrees` means the worktree list, or what `git worktree list` reports per worktree, changed:\n * `worktrees`, `worktrees/<n>` (add/remove/prune) and `worktrees/<n>/{HEAD,locked,gitdir}`. Any other\n * path under `worktrees/<n>/` (index, `*_HEAD`, `MERGE_*`, rebase state, logs, …) is that worktree's\n * own state, which its own gitdir watcher classifies, so it yields `undefined` here.\n *\n * @param relativePath - Path relative to the common `.git` directory or a worktree's gitdir\n * (e.g., `refs/heads/main`, `HEAD`, `config`, `worktrees/foo/HEAD`)\n * @returns An array of change types, or `undefined` if the path\n * is noise or unrecognized. `undefined` for `FETCH_HEAD` which\n * is intentionally not mapped to a change type (the extension\n * handles it as a last-fetched timestamp).\n */\nexport function classifyGitDirChange(relativePath: string): RepositoryChange[] | undefined {\n\t// User-edited todo file (and its backup) — handled by VS Code's TextDocument\n\t// events in the rebase webview (see `rebaseWebviewProvider.ts`). Suppress here\n\t// to avoid thrashing the repo update path on every save during an interactive\n\t// rebase; rebase lifecycle (start/stop/step) still fires from sibling files\n\t// in `rebase-merge/` and the directory itself.\n\tif (relativePath.endsWith('/git-rebase-todo') || relativePath.endsWith('/git-rebase-todo.backup')) {\n\t\treturn undefined;\n\t}\n\n\tif (relativePath.startsWith('worktrees')) {\n\t\tif (relativePath.length === 9) return ['worktrees'];\n\n\t\tif (relativePath.charCodeAt(9) === 47 /* / */) {\n\t\t\tconst idEnd = relativePath.indexOf('/', 10);\n\t\t\tif (idEnd === -1) return relativePath.length > 10 ? ['worktrees'] : undefined;\n\n\t\t\tconst file = relativePath.substring(idEnd + 1);\n\t\t\treturn file === 'HEAD' || file === 'locked' || file === 'gitdir' ? ['worktrees'] : undefined;\n\t\t}\n\t}\n\n\t// A ref outside the namespaces core reads (a tool's own, such as `refs/undo/*`) is no change, whatever words its\n\t// name holds: `refs/kepler/x/fix-index` must not read as the index\n\tif (relativePath.startsWith('refs/')) {\n\t\tif (!watchedRefRegex.test(relativePath)) return undefined;\n\n\t\t// A ref's lock file is no change of the ref (no ref name ends in `.lock`): git renames it over the ref, which\n\t\t// reports the ref itself, or removes it with the ref unchanged, as every fetch does with\n\t\t// `refs/remotes/<remote>/HEAD` to check the remote's default branch. Except the stash's: its entries are its\n\t\t// reflog (`logs/`, unwatched), which `git reflog expire` (as `gc` runs it) and `git reflog delete` rewrite under\n\t\t// that lock alone\n\t\tif (relativePath.endsWith('.lock') && relativePath !== 'refs/stash.lock') return undefined;\n\t}\n\n\tconst match = classifyRegex.exec(relativePath);\n\tif (match == null) return undefined;\n\n\tswitch (match[1]) {\n\t\tcase 'config':\n\t\t\treturn ['config', 'remotes'];\n\n\t\tcase 'gk/config':\n\t\t\treturn ['gkConfig'];\n\n\t\tcase 'info/exclude':\n\t\t\treturn ['ignores'];\n\n\t\tcase 'index':\n\t\t\treturn ['index'];\n\n\t\tcase 'FETCH_HEAD':\n\t\t\t// No RepositoryChange — extension handles this for last-fetched timestamps\n\t\t\treturn undefined;\n\n\t\tcase 'HEAD':\n\t\t\treturn ['head', 'heads'];\n\n\t\tcase 'ORIG_HEAD':\n\t\t\treturn ['heads'];\n\n\t\tcase 'shallow':\n\t\tcase 'refs/replace':\n\t\t\t// Ancestry changes affect graph, blame/history, health, and ahead/behind consumers even\n\t\t\t// when tips stay put. Reuse their shared history invalidation rather than only refreshing Graph.\n\t\t\treturn ['heads'];\n\n\t\tcase 'CHERRY_PICK_HEAD':\n\t\t\treturn ['cherryPick', 'pausedOp'];\n\n\t\tcase 'MERGE_HEAD':\n\t\t\treturn ['merge', 'pausedOp'];\n\n\t\tcase 'REBASE_HEAD':\n\t\tcase 'rebase-merge':\n\t\tcase 'rebase-apply':\n\t\t\treturn ['rebase', 'pausedOp'];\n\n\t\tcase 'REVERT_HEAD':\n\t\t\treturn ['revert', 'pausedOp'];\n\n\t\tcase 'sequencer':\n\t\t\treturn ['pausedOp'];\n\n\t\t// `'heads'` is deliberately the ONLY thing a `refs/heads/*` event yields, including a create. It is\n\t\t// tempting to read a create as \"a new branch exists under this name\" and use it to drop anything\n\t\t// derived from the old branch's identity (its base, its merge target). That signal does not mean\n\t\t// what it looks like: git rewrites an EXISTING loose ref by writing `<name>.lock` and renaming it\n\t\t// over the file, which watchers report as a create of the destination — so every ordinary commit\n\t\t// would look like a new branch and discard a live branch's metadata. Nothing here can distinguish\n\t\t// the two; identity changes are handled where GitLens performs the operation itself.\n\t\tcase 'refs/heads':\n\t\t\treturn ['heads'];\n\n\t\tcase 'refs/remotes':\n\t\t\treturn ['remotes'];\n\n\t\tcase 'refs/stash':\n\t\t\treturn ['stash'];\n\n\t\tcase 'refs/tags':\n\t\t\treturn ['tags'];\n\n\t\t// A `packed-refs` rewrite (after `git pack-refs` or `git gc`) changes ref state\n\t\t// WITHOUT any `refs/**` file event — deleting a packed branch, for example, only\n\t\t// rewrites this file. The file content is not inspected here, so classification\n\t\t// is deliberately broad: it can add, remove, or move refs of any of these types.\n\t\tcase 'packed-refs':\n\t\t\treturn ['heads', 'remotes', 'tags'];\n\n\t\tdefault:\n\t\t\treturn undefined;\n\t}\n}\n"],"mappings":";;;;;;AAOA,MAAa,8BAA2D;CACvE;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACD;AAEA,MAAM,gBACL;;AAED,MAAM,kBAAkB;;;;;;;;;;;;;;;;;;;AAoBxB,SAAgB,qBAAqB,cAAsD;CAM1F,IAAI,aAAa,SAAS,kBAAkB,KAAK,aAAa,SAAS,yBAAyB,GAC/F;CAGD,IAAI,aAAa,WAAW,WAAW,GAAG;EACzC,IAAI,aAAa,WAAW,GAAG,OAAO,CAAC,WAAW;EAElD,IAAI,aAAa,WAAW,CAAC,MAAM,IAAY;GAC9C,MAAM,QAAQ,aAAa,QAAQ,KAAK,EAAE;GAC1C,IAAI,UAAU,IAAI,OAAO,aAAa,SAAS,KAAK,CAAC,WAAW,IAAI,KAAA;GAEpE,MAAM,OAAO,aAAa,UAAU,QAAQ,CAAC;GAC7C,OAAO,SAAS,UAAU,SAAS,YAAY,SAAS,WAAW,CAAC,WAAW,IAAI,KAAA;EACpF;CACD;CAIA,IAAI,aAAa,WAAW,OAAO,GAAG;EACrC,IAAI,CAAC,gBAAgB,KAAK,YAAY,GAAG,OAAO,KAAA;EAOhD,IAAI,aAAa,SAAS,OAAO,KAAK,iBAAiB,mBAAmB,OAAO,KAAA;CAClF;CAEA,MAAM,QAAQ,cAAc,KAAK,YAAY;CAC7C,IAAI,SAAS,MAAM,OAAO,KAAA;CAE1B,QAAQ,MAAM,IAAd;EACC,KAAK,UACJ,OAAO,CAAC,UAAU,SAAS;EAE5B,KAAK,aACJ,OAAO,CAAC,UAAU;EAEnB,KAAK,gBACJ,OAAO,CAAC,SAAS;EAElB,KAAK,SACJ,OAAO,CAAC,OAAO;EAEhB,KAAK,cAEJ;EAED,KAAK,QACJ,OAAO,CAAC,QAAQ,OAAO;EAExB,KAAK,aACJ,OAAO,CAAC,OAAO;EAEhB,KAAK;EACL,KAAK,gBAGJ,OAAO,CAAC,OAAO;EAEhB,KAAK,oBACJ,OAAO,CAAC,cAAc,UAAU;EAEjC,KAAK,cACJ,OAAO,CAAC,SAAS,UAAU;EAE5B,KAAK;EACL,KAAK;EACL,KAAK,gBACJ,OAAO,CAAC,UAAU,UAAU;EAE7B,KAAK,eACJ,OAAO,CAAC,UAAU,UAAU;EAE7B,KAAK,aACJ,OAAO,CAAC,UAAU;EASnB,KAAK,cACJ,OAAO,CAAC,OAAO;EAEhB,KAAK,gBACJ,OAAO,CAAC,SAAS;EAElB,KAAK,cACJ,OAAO,CAAC,OAAO;EAEhB,KAAK,aACJ,OAAO,CAAC,MAAM;EAMf,KAAK,eACJ,OAAO;GAAC;GAAS;GAAW;EAAM;EAEnC,SACC;CACF;AACD"}
@@ -372,7 +372,8 @@ function isConfigFile(relativePath) {
372
372
  }
373
373
  /**
374
374
  * The ref a `.git`-relative path names: a loose ref (or ref directory) under `refs/`, or the `HEAD`/`ORIG_HEAD`
375
- * pseudo-refs. Git writes a ref as `<name>.lock` and renames it over `<name>`, so the lock file names the same ref.
375
+ * pseudo-refs. A lock file names the ref it locks; the only ones classification lets through are the pseudo-refs' and
376
+ * `refs/stash.lock`.
376
377
  */
377
378
  function getChangedRef(relativePath) {
378
379
  const ref = relativePath.endsWith(".lock") ? relativePath.substring(0, relativePath.length - 5) : relativePath;
@@ -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 { some } from '@gitlens/utils/iterable.js';\nimport { isDescendant, joinPaths, relative } from '@gitlens/utils/path.js';\nimport type { GitDir, RepositoryChange } from '../models/repository.js';\nimport { allWatchedRepositoryChanges, classifyGitDirChange } from './classifyChange.js';\nimport type { GitConfigSections, GitConfigSectionsChange } from './configSections.js';\nimport { diffGitConfigSections, parseGitConfigBytes } from './configSections.js';\nimport type { GitDirPoller, GitDirProbe } from './gitDirPoller.js';\nimport { GitDirProbeTarget } from './gitDirPoller.js';\nimport type {\n\tFileWatcher,\n\tFileWatcherOptions,\n\tFileWatcherReliability,\n\tFileWatchEvent,\n\tFileWatchingProvider,\n} from './provider.js';\nimport {\n\tdotGitGlobCombined,\n\tdotGitGlobCommon,\n\tdotGitGlobRoot,\n\tgetGitDirRefProbes,\n\tgitDirCommonProbes,\n\tgitDirExcludes,\n\tgitDirProbesByEmittedPath,\n\tgitDirRootProbes,\n\tshouldIgnoreRepoPath,\n} from './watcherPatterns.js';\n\n/**\n * How long a read of the shared `.git/config` may take before it counts as failed (unconfined). Reads run one at a\n * time, so one that never settles, as on a hung network or WSL mount, would otherwise hold back every later report.\n */\nconst configReadTimeoutMs = 5000;\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; `false` when config changed without moving refs, and `undefined` when ref movement\n\t * is unknown. `overflow` marks the every-kind rescan of a watcher that dropped events, which no single write can\n\t * account for. `configBranches`, with a `config` change, are the local branches (full ref names) whose\n\t * `branch.<name>` sections are all that changed in the shared `.git/config`; without it, the config change may be\n\t * anywhere.\n\t */\n\treadonly onRepoChanged: (\n\t\trepoPath: string,\n\t\tchanges: RepositoryChange[],\n\t\tref?: string | false,\n\t\toverflow?: boolean,\n\t\tconfigBranches?: ReadonlySet<string>,\n\t) => void;\n\treadonly onFetchHeadChanged?: (repoPath: string) => void;\n\treadonly onIgnoresChanged?: (repoPath: string) => void;\n\t/** The refs whose loose files a poll stats for this session, read at each poll */\n\treadonly getProbeRefs?: () => Iterable<string>;\n\t/** A watcher this session hears through went `unwatchable`, or stopped being it (see {@link WatchGroup.isUnwatchable}) */\n\treadonly onUnwatchableChanged?: (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\treliability: FileWatcherReliability;\n\t/** Polls in place of (or beside) the root watcher while it isn't `reliable` */\n\ttarget?: GitDirProbeTarget;\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 *\n * Given a `readFile`, the group keeps the shared `.git/config` as it last read it, section by section. A report of\n * that file (or its lock), from whichever watcher, is read and compared with it once, and every session hears what\n * changed: only some branches' `branch.<name>` sections (`config`, confined to them), anything else (`config` and\n * `remotes`, unconfined), or nothing at all, when an earlier report's read already delivered the change. Reads run\n * one at a time, so each compares with the one before it and none is skipped. The first read, taken as the group\n * starts, is the baseline and delivers nothing; a report arriving before it completes may describe a change it\n * already holds, so it is delivered unconfined. A read that fails or doesn't settle within a few seconds, or a file\n * git couldn't parse, is unconfined too.\n */\nexport class WatchGroup implements UnifiedDisposable {\n\tprivate readonly _sessions = new Map<string, SessionEntry>();\n\tprivate _commonWatcher: FileWatcher | undefined;\n\tprivate _commonReliability: FileWatcherReliability = 'reliable';\n\tprivate _commonTarget: GitDirProbeTarget | undefined;\n\tprivate _disposed = false;\n\n\t/** The shared `.git/config`, by section, as the last read that could parse it found it */\n\tprivate _config: GitConfigSections | undefined;\n\t/** The baseline read is in flight */\n\tprivate _configBaselining = false;\n\tprivate _configReading = false;\n\tprivate _configRereadQueued = false;\n\n\tconstructor(\n\t\treadonly commonGitDir: string,\n\t\tprivate readonly provider: FileWatchingProvider,\n\t\t/** When given, a watcher that reports it can't be trusted is polled; otherwise none is asked */\n\t\tprivate readonly poller?: GitDirPoller,\n\t\t/** When given, reports of the shared `.git/config` are classified by what changed in it (see above) */\n\t\tprivate readonly readFile?: (path: string) => Promise<Uint8Array>,\n\t) {\n\t\tif (readFile != null) {\n\t\t\tvoid this.readConfigBaseline();\n\t\t}\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\tthis.stopPolling(entry);\n\t\t}\n\t\tthis._sessions.clear();\n\n\t\tthis.disposeCommonWatcher();\n\t}\n\n\tget sessions(): ReadonlyMap<string, SessionEntry> {\n\t\treturn this._sessions;\n\t}\n\n\t/** True when a watcher the session hears through — its root watcher, or a linked session's common one — can't watch */\n\tisUnwatchable(repoPath: string): boolean {\n\t\tconst entry = this._sessions.get(repoPath);\n\t\tif (entry == null) return false;\n\n\t\treturn (\n\t\t\tentry.reliability === 'unwatchable' || (!entry.coversCommon && this._commonReliability === 'unwatchable')\n\t\t);\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\t// The watcher can report its reliability before `createWatcher` returns, so before the entry exists\n\t\tlet entry: SessionEntry | undefined;\n\t\tlet initialReliability: FileWatcherReliability = 'reliable';\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 e = this._sessions.get(repoPath);\n\t\t\t\tif (e != null) {\n\t\t\t\t\tif (event.reason !== 'overflow') {\n\t\t\t\t\t\te.target?.forget(event.path);\n\t\t\t\t\t}\n\t\t\t\t\tthis.onRootEvent(event, e);\n\t\t\t\t}\n\t\t\t},\n\t\t\tthis.getWatcherOptions(state => {\n\t\t\t\tif (entry == null) {\n\t\t\t\t\tinitialReliability = state;\n\t\t\t\t} else if (this._sessions.get(repoPath) === entry) {\n\t\t\t\t\tthis.setRootReliability(entry, state);\n\t\t\t\t}\n\t\t\t}),\n\t\t);\n\n\t\tconst entryGitDirPath = gitDir.uri.fsPath;\n\t\tentry = {\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\treliability: 'reliable',\n\t\t};\n\n\t\tthis._sessions.set(repoPath, entry);\n\t\tif (initialReliability !== 'reliable') {\n\t\t\tthis.setRootReliability(entry, initialReliability, true);\n\t\t}\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.stopPolling(entry);\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.disposeCommonWatcher();\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\t// The watcher can report its reliability before `createWatcher` returns\n\t\tlet watcher: FileWatcher | undefined;\n\t\tlet initialReliability: FileWatcherReliability = 'reliable';\n\t\tconst options = this.getWatcherOptions(state => {\n\t\t\tif (watcher == null) {\n\t\t\t\tinitialReliability = state;\n\t\t\t} else if (this._commonWatcher === watcher) {\n\t\t\t\tthis.setCommonReliability(state);\n\t\t\t}\n\t\t});\n\n\t\twatcher = this.provider.createWatcher(\n\t\t\tthis.commonGitDir,\n\t\t\tdotGitGlobCommon,\n\t\t\t(event: FileWatchEvent) => {\n\t\t\t\tif (event.reason !== 'overflow') {\n\t\t\t\t\tthis._commonTarget?.forget(event.path);\n\t\t\t\t}\n\t\t\t\tthis.onCommonEvent(event);\n\t\t\t},\n\t\t\toptions,\n\t\t);\n\t\tthis._commonWatcher = watcher;\n\t\tif (initialReliability !== 'reliable') {\n\t\t\tthis.setCommonReliability(initialReliability, true);\n\t\t}\n\t}\n\n\tprivate disposeCommonWatcher(): void {\n\t\tthis._commonWatcher?.dispose();\n\t\tthis._commonWatcher = undefined;\n\t\tthis._commonReliability = 'reliable';\n\t\tif (this._commonTarget != null) {\n\t\t\tthis.poller?.delete(this._commonTarget);\n\t\t\tthis._commonTarget = undefined;\n\t\t}\n\t}\n\n\t/** Hosts that didn't opt into polling get exactly the options they always had */\n\tprivate getWatcherOptions(onDidChangeReliability: (state: FileWatcherReliability) => void): FileWatcherOptions {\n\t\treturn this.poller != null\n\t\t\t? { excludes: gitDirExcludes, onDidChangeReliability: onDidChangeReliability }\n\t\t\t: { excludes: gitDirExcludes };\n\t}\n\n\tprivate setRootReliability(entry: SessionEntry, state: FileWatcherReliability, initial?: boolean): void {\n\t\tconst previous = entry.reliability;\n\t\tif (previous === state || this.poller == null) return;\n\n\t\tentry.reliability = state;\n\t\tif (state === 'reliable') {\n\t\t\tthis.stopPolling(entry);\n\t\t} else if (entry.target == null) {\n\t\t\tconst target = new GitDirProbeTarget(\n\t\t\t\tentry.gitDirPath,\n\t\t\t\tthis.poller,\n\t\t\t\t() => this.getRootProbes(entry),\n\t\t\t\tgitDirProbesByEmittedPath,\n\t\t\t\t(event, probe) => {\n\t\t\t\t\tif (entry.target !== target) return;\n\n\t\t\t\t\tthis.onRootEvent(event, entry);\n\t\t\t\t\tif (probe.path === 'logs/HEAD' && !entry.coversCommon) {\n\t\t\t\t\t\tthis.onLinkedHeadMoved();\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t);\n\t\t\tentry.target = target;\n\t\t\tthis.poller.add(target);\n\t\t}\n\n\t\tif ((state === 'unwatchable') === (previous === 'unwatchable')) return;\n\n\t\t// The watch went down or came back: whatever changed meanwhile went unseen\n\t\tif (!initial) {\n\t\t\tthis.deliverOverflow(entry);\n\t\t}\n\t\tentry.hooks.onUnwatchableChanged?.(entry.repoPath);\n\t}\n\n\tprivate setCommonReliability(state: FileWatcherReliability, initial?: boolean): void {\n\t\tconst previous = this._commonReliability;\n\t\tif (previous === state || this.poller == null) return;\n\n\t\tthis._commonReliability = state;\n\t\tif (state === 'reliable') {\n\t\t\tif (this._commonTarget != null) {\n\t\t\t\tthis.poller.delete(this._commonTarget);\n\t\t\t\tthis._commonTarget = undefined;\n\t\t\t}\n\t\t} else if (this._commonTarget == null) {\n\t\t\tconst target = new GitDirProbeTarget(\n\t\t\t\tthis.commonGitDir,\n\t\t\t\tthis.poller,\n\t\t\t\t() => this.getCommonProbes(),\n\t\t\t\tgitDirProbesByEmittedPath,\n\t\t\t\tevent => {\n\t\t\t\t\tif (this._commonTarget === target) {\n\t\t\t\t\t\tthis.onCommonEvent(event);\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t);\n\t\t\tthis._commonTarget = target;\n\t\t\tthis.poller.add(target);\n\t\t}\n\n\t\tif ((state === 'unwatchable') === (previous === 'unwatchable')) return;\n\n\t\tfor (const entry of this._sessions.values()) {\n\t\t\tif (entry.coversCommon) continue;\n\n\t\t\tif (!initial) {\n\t\t\t\tthis.deliverOverflow(entry);\n\t\t\t}\n\t\t\tentry.hooks.onUnwatchableChanged?.(entry.repoPath);\n\t\t}\n\t}\n\n\tprivate stopPolling(entry: SessionEntry): void {\n\t\tif (entry.target == null) return;\n\n\t\tthis.poller?.delete(entry.target);\n\t\tentry.target = undefined;\n\t}\n\n\t/**\n\t * A session rooted at the common dir polls the common files too, with the refs of every session in the group: its\n\t * combined watcher would hear them all. While the common dir is polled too, the poller stats each shared file once\n\t * for both, and each keeps its own readings, so what one watcher reported is held back from its own sessions only\n\t */\n\tprivate getRootProbes(entry: SessionEntry): readonly GitDirProbe[] {\n\t\tif (!entry.coversCommon) return gitDirRootProbes;\n\n\t\tconst refs = new Set<string>();\n\t\tfor (const e of this._sessions.values()) {\n\t\t\tfor (const ref of e.hooks.getProbeRefs?.() ?? []) {\n\t\t\t\trefs.add(ref);\n\t\t\t}\n\t\t}\n\t\treturn [...gitDirRootProbes, ...gitDirCommonProbes, ...getGitDirRefProbes(refs)];\n\t}\n\n\t/**\n\t * A linked worktree's `logs/HEAD` moved — a commit or reset there, on a branch a polled session rooted at the common\n\t * dir may not probe — so each such session hears a `heads` change its combined watcher would have heard\n\t */\n\tprivate onLinkedHeadMoved(): void {\n\t\tfor (const entry of this._sessions.values()) {\n\t\t\tif (!entry.coversCommon || entry.target == null) continue;\n\n\t\t\tentry.hooks.onRepoChanged(entry.repoPath, ['heads']);\n\t\t}\n\t}\n\n\t/** The common files, with the refs of every linked session (the only ones the common watcher delivers to) */\n\tprivate getCommonProbes(): GitDirProbe[] {\n\t\tconst refs = new Set<string>();\n\t\tfor (const entry of this._sessions.values()) {\n\t\t\tif (entry.coversCommon) continue;\n\n\t\t\tfor (const ref of entry.hooks.getProbeRefs?.() ?? []) {\n\t\t\t\trefs.add(ref);\n\t\t\t}\n\t\t}\n\t\treturn [...gitDirCommonProbes, ...getGitDirRefProbes(refs)];\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\tif (this.readFile != null && isConfigFile(relativePath)) {\n\t\t\tthis.onConfigReported();\n\t\t\treturn;\n\t\t}\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], undefined, true);\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\t// A linked worktree's git dir isn't the common one, so no file there is the shared config\n\t\tif (entry.coversCommon && this.readFile != null && isConfigFile(relativePath)) {\n\t\t\tthis.onConfigReported();\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\tprivate onConfigReported(): void {\n\t\t// The baseline may already hold the change reported, which comparing with it would lose\n\t\tif (this._configBaselining) {\n\t\t\tthis.deliverConfigChange(undefined);\n\t\t\treturn;\n\t\t}\n\t\tif (this._configReading) {\n\t\t\tthis._configRereadQueued = true;\n\t\t\treturn;\n\t\t}\n\n\t\tvoid this.readConfigChanges();\n\t}\n\n\tprivate async readConfigBaseline(): Promise<void> {\n\t\tthis._configBaselining = true;\n\t\tthis._configReading = true;\n\t\ttry {\n\t\t\tconst config = await this.readConfig();\n\t\t\tif (this._disposed) return;\n\n\t\t\tthis._config = config;\n\t\t} finally {\n\t\t\tthis._configBaselining = false;\n\t\t\tthis._configReading = false;\n\t\t}\n\t}\n\n\t/** Reads and compares until no report came in during the last read: it may describe a change the read missed */\n\tprivate async readConfigChanges(): Promise<void> {\n\t\tthis._configReading = true;\n\t\ttry {\n\t\t\tdo {\n\t\t\t\tthis._configRereadQueued = false;\n\t\t\t\tconst config = await this.readConfig();\n\t\t\t\tif (this._disposed) return;\n\n\t\t\t\tconst previous = this._config;\n\t\t\t\tif (config == null || previous == null) {\n\t\t\t\t\t// Unreadable now, or never readable before: the change may be anywhere. A failed read keeps the last\n\t\t\t\t\t// snapshot, so the next read still compares with what every session was told\n\t\t\t\t\tthis._config ??= config;\n\t\t\t\t\tthis.deliverConfigChange(undefined);\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\tthis._config = config;\n\t\t\t\tconst change = diffGitConfigSections(previous, config);\n\t\t\t\tif (change != null) {\n\t\t\t\t\tthis.deliverConfigChange(change);\n\t\t\t\t}\n\t\t\t} while (this._configRereadQueued);\n\t\t} finally {\n\t\t\tthis._configReading = false;\n\t\t}\n\t}\n\n\t/** `undefined` when the file couldn't be read or parsed, or the read didn't settle in time */\n\tprivate async readConfig(): Promise<GitConfigSections | undefined> {\n\t\tconst readFile = this.readFile;\n\t\tif (readFile == null) return undefined;\n\n\t\tlet timer: ReturnType<typeof setTimeout> | undefined;\n\t\ttry {\n\t\t\tconst data = await Promise.race([\n\t\t\t\treadFile(joinPaths(this.commonGitDir, 'config')),\n\t\t\t\tnew Promise<undefined>(resolve => {\n\t\t\t\t\ttimer = setTimeout(resolve, configReadTimeoutMs, undefined);\n\t\t\t\t}),\n\t\t\t]);\n\t\t\treturn data != null ? parseGitConfigBytes(data) : undefined;\n\t\t} catch {\n\t\t\treturn undefined;\n\t\t} finally {\n\t\t\tclearTimeout(timer);\n\t\t}\n\t}\n\n\t/**\n\t * Tells every session of a change to the shared `.git/config`: confined to some branches' sections, or (`undefined`,\n\t * or a change outside them) anywhere, which may have changed a remote's settings too. Either way no ref moved\n\t */\n\tprivate deliverConfigChange(change: GitConfigSectionsChange | undefined): void {\n\t\tfor (const entry of this._sessions.values()) {\n\t\t\tif (change?.confined) {\n\t\t\t\tentry.hooks.onRepoChanged(entry.repoPath, ['config'], false, false, change.branches);\n\t\t\t} else {\n\t\t\t\tentry.hooks.onRepoChanged(entry.repoPath, ['config', 'remotes'], false);\n\t\t\t}\n\t\t}\n\t}\n}\n\n/** The shared `.git/config` (or the lock git writes it through), relative to the common git dir */\nfunction isConfigFile(relativePath: string): boolean {\n\treturn relativePath === 'config' || relativePath === 'config.lock';\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 | false | undefined {\n\tconst ref = relativePath.endsWith('.lock') ? relativePath.substring(0, relativePath.length - 5) : relativePath;\n\t// Remote config still invalidates remote and branch caches, but doesn't itself move any ref.\n\tif (ref === 'config' || ref === 'config.worktree') return false;\n\n\treturn ref.startsWith('refs/') || ref === 'HEAD' || ref === 'ORIG_HEAD' ? ref : undefined;\n}\n"],"mappings":"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"}
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, joinPaths, relative } from '@gitlens/utils/path.js';\nimport type { GitDir, RepositoryChange } from '../models/repository.js';\nimport { allWatchedRepositoryChanges, classifyGitDirChange } from './classifyChange.js';\nimport type { GitConfigSections, GitConfigSectionsChange } from './configSections.js';\nimport { diffGitConfigSections, parseGitConfigBytes } from './configSections.js';\nimport type { GitDirPoller, GitDirProbe } from './gitDirPoller.js';\nimport { GitDirProbeTarget } from './gitDirPoller.js';\nimport type {\n\tFileWatcher,\n\tFileWatcherOptions,\n\tFileWatcherReliability,\n\tFileWatchEvent,\n\tFileWatchingProvider,\n} from './provider.js';\nimport {\n\tdotGitGlobCombined,\n\tdotGitGlobCommon,\n\tdotGitGlobRoot,\n\tgetGitDirRefProbes,\n\tgitDirCommonProbes,\n\tgitDirExcludes,\n\tgitDirProbesByEmittedPath,\n\tgitDirRootProbes,\n\tshouldIgnoreRepoPath,\n} from './watcherPatterns.js';\n\n/**\n * How long a read of the shared `.git/config` may take before it counts as failed (unconfined). Reads run one at a\n * time, so one that never settles, as on a hung network or WSL mount, would otherwise hold back every later report.\n */\nconst configReadTimeoutMs = 5000;\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; `false` when config changed without moving refs, and `undefined` when ref movement\n\t * is unknown. `overflow` marks the every-kind rescan of a watcher that dropped events, which no single write can\n\t * account for. `configBranches`, with a `config` change, are the local branches (full ref names) whose\n\t * `branch.<name>` sections are all that changed in the shared `.git/config`; without it, the config change may be\n\t * anywhere.\n\t */\n\treadonly onRepoChanged: (\n\t\trepoPath: string,\n\t\tchanges: RepositoryChange[],\n\t\tref?: string | false,\n\t\toverflow?: boolean,\n\t\tconfigBranches?: ReadonlySet<string>,\n\t) => void;\n\treadonly onFetchHeadChanged?: (repoPath: string) => void;\n\treadonly onIgnoresChanged?: (repoPath: string) => void;\n\t/** The refs whose loose files a poll stats for this session, read at each poll */\n\treadonly getProbeRefs?: () => Iterable<string>;\n\t/** A watcher this session hears through went `unwatchable`, or stopped being it (see {@link WatchGroup.isUnwatchable}) */\n\treadonly onUnwatchableChanged?: (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\treliability: FileWatcherReliability;\n\t/** Polls in place of (or beside) the root watcher while it isn't `reliable` */\n\ttarget?: GitDirProbeTarget;\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 *\n * Given a `readFile`, the group keeps the shared `.git/config` as it last read it, section by section. A report of\n * that file (or its lock), from whichever watcher, is read and compared with it once, and every session hears what\n * changed: only some branches' `branch.<name>` sections (`config`, confined to them), anything else (`config` and\n * `remotes`, unconfined), or nothing at all, when an earlier report's read already delivered the change. Reads run\n * one at a time, so each compares with the one before it and none is skipped. The first read, taken as the group\n * starts, is the baseline and delivers nothing; a report arriving before it completes may describe a change it\n * already holds, so it is delivered unconfined. A read that fails or doesn't settle within a few seconds, or a file\n * git couldn't parse, is unconfined too.\n */\nexport class WatchGroup implements UnifiedDisposable {\n\tprivate readonly _sessions = new Map<string, SessionEntry>();\n\tprivate _commonWatcher: FileWatcher | undefined;\n\tprivate _commonReliability: FileWatcherReliability = 'reliable';\n\tprivate _commonTarget: GitDirProbeTarget | undefined;\n\tprivate _disposed = false;\n\n\t/** The shared `.git/config`, by section, as the last read that could parse it found it */\n\tprivate _config: GitConfigSections | undefined;\n\t/** The baseline read is in flight */\n\tprivate _configBaselining = false;\n\tprivate _configReading = false;\n\tprivate _configRereadQueued = false;\n\n\tconstructor(\n\t\treadonly commonGitDir: string,\n\t\tprivate readonly provider: FileWatchingProvider,\n\t\t/** When given, a watcher that reports it can't be trusted is polled; otherwise none is asked */\n\t\tprivate readonly poller?: GitDirPoller,\n\t\t/** When given, reports of the shared `.git/config` are classified by what changed in it (see above) */\n\t\tprivate readonly readFile?: (path: string) => Promise<Uint8Array>,\n\t) {\n\t\tif (readFile != null) {\n\t\t\tvoid this.readConfigBaseline();\n\t\t}\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\tthis.stopPolling(entry);\n\t\t}\n\t\tthis._sessions.clear();\n\n\t\tthis.disposeCommonWatcher();\n\t}\n\n\tget sessions(): ReadonlyMap<string, SessionEntry> {\n\t\treturn this._sessions;\n\t}\n\n\t/** True when a watcher the session hears through — its root watcher, or a linked session's common one — can't watch */\n\tisUnwatchable(repoPath: string): boolean {\n\t\tconst entry = this._sessions.get(repoPath);\n\t\tif (entry == null) return false;\n\n\t\treturn (\n\t\t\tentry.reliability === 'unwatchable' || (!entry.coversCommon && this._commonReliability === 'unwatchable')\n\t\t);\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\t// The watcher can report its reliability before `createWatcher` returns, so before the entry exists\n\t\tlet entry: SessionEntry | undefined;\n\t\tlet initialReliability: FileWatcherReliability = 'reliable';\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 e = this._sessions.get(repoPath);\n\t\t\t\tif (e != null) {\n\t\t\t\t\tif (event.reason !== 'overflow') {\n\t\t\t\t\t\te.target?.forget(event.path);\n\t\t\t\t\t}\n\t\t\t\t\tthis.onRootEvent(event, e);\n\t\t\t\t}\n\t\t\t},\n\t\t\tthis.getWatcherOptions(state => {\n\t\t\t\tif (entry == null) {\n\t\t\t\t\tinitialReliability = state;\n\t\t\t\t} else if (this._sessions.get(repoPath) === entry) {\n\t\t\t\t\tthis.setRootReliability(entry, state);\n\t\t\t\t}\n\t\t\t}),\n\t\t);\n\n\t\tconst entryGitDirPath = gitDir.uri.fsPath;\n\t\tentry = {\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\treliability: 'reliable',\n\t\t};\n\n\t\tthis._sessions.set(repoPath, entry);\n\t\tif (initialReliability !== 'reliable') {\n\t\t\tthis.setRootReliability(entry, initialReliability, true);\n\t\t}\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.stopPolling(entry);\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.disposeCommonWatcher();\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\t// The watcher can report its reliability before `createWatcher` returns\n\t\tlet watcher: FileWatcher | undefined;\n\t\tlet initialReliability: FileWatcherReliability = 'reliable';\n\t\tconst options = this.getWatcherOptions(state => {\n\t\t\tif (watcher == null) {\n\t\t\t\tinitialReliability = state;\n\t\t\t} else if (this._commonWatcher === watcher) {\n\t\t\t\tthis.setCommonReliability(state);\n\t\t\t}\n\t\t});\n\n\t\twatcher = this.provider.createWatcher(\n\t\t\tthis.commonGitDir,\n\t\t\tdotGitGlobCommon,\n\t\t\t(event: FileWatchEvent) => {\n\t\t\t\tif (event.reason !== 'overflow') {\n\t\t\t\t\tthis._commonTarget?.forget(event.path);\n\t\t\t\t}\n\t\t\t\tthis.onCommonEvent(event);\n\t\t\t},\n\t\t\toptions,\n\t\t);\n\t\tthis._commonWatcher = watcher;\n\t\tif (initialReliability !== 'reliable') {\n\t\t\tthis.setCommonReliability(initialReliability, true);\n\t\t}\n\t}\n\n\tprivate disposeCommonWatcher(): void {\n\t\tthis._commonWatcher?.dispose();\n\t\tthis._commonWatcher = undefined;\n\t\tthis._commonReliability = 'reliable';\n\t\tif (this._commonTarget != null) {\n\t\t\tthis.poller?.delete(this._commonTarget);\n\t\t\tthis._commonTarget = undefined;\n\t\t}\n\t}\n\n\t/** Hosts that didn't opt into polling get exactly the options they always had */\n\tprivate getWatcherOptions(onDidChangeReliability: (state: FileWatcherReliability) => void): FileWatcherOptions {\n\t\treturn this.poller != null\n\t\t\t? { excludes: gitDirExcludes, onDidChangeReliability: onDidChangeReliability }\n\t\t\t: { excludes: gitDirExcludes };\n\t}\n\n\tprivate setRootReliability(entry: SessionEntry, state: FileWatcherReliability, initial?: boolean): void {\n\t\tconst previous = entry.reliability;\n\t\tif (previous === state || this.poller == null) return;\n\n\t\tentry.reliability = state;\n\t\tif (state === 'reliable') {\n\t\t\tthis.stopPolling(entry);\n\t\t} else if (entry.target == null) {\n\t\t\tconst target = new GitDirProbeTarget(\n\t\t\t\tentry.gitDirPath,\n\t\t\t\tthis.poller,\n\t\t\t\t() => this.getRootProbes(entry),\n\t\t\t\tgitDirProbesByEmittedPath,\n\t\t\t\t(event, probe) => {\n\t\t\t\t\tif (entry.target !== target) return;\n\n\t\t\t\t\tthis.onRootEvent(event, entry);\n\t\t\t\t\tif (probe.path === 'logs/HEAD' && !entry.coversCommon) {\n\t\t\t\t\t\tthis.onLinkedHeadMoved();\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t);\n\t\t\tentry.target = target;\n\t\t\tthis.poller.add(target);\n\t\t}\n\n\t\tif ((state === 'unwatchable') === (previous === 'unwatchable')) return;\n\n\t\t// The watch went down or came back: whatever changed meanwhile went unseen\n\t\tif (!initial) {\n\t\t\tthis.deliverOverflow(entry);\n\t\t}\n\t\tentry.hooks.onUnwatchableChanged?.(entry.repoPath);\n\t}\n\n\tprivate setCommonReliability(state: FileWatcherReliability, initial?: boolean): void {\n\t\tconst previous = this._commonReliability;\n\t\tif (previous === state || this.poller == null) return;\n\n\t\tthis._commonReliability = state;\n\t\tif (state === 'reliable') {\n\t\t\tif (this._commonTarget != null) {\n\t\t\t\tthis.poller.delete(this._commonTarget);\n\t\t\t\tthis._commonTarget = undefined;\n\t\t\t}\n\t\t} else if (this._commonTarget == null) {\n\t\t\tconst target = new GitDirProbeTarget(\n\t\t\t\tthis.commonGitDir,\n\t\t\t\tthis.poller,\n\t\t\t\t() => this.getCommonProbes(),\n\t\t\t\tgitDirProbesByEmittedPath,\n\t\t\t\tevent => {\n\t\t\t\t\tif (this._commonTarget === target) {\n\t\t\t\t\t\tthis.onCommonEvent(event);\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t);\n\t\t\tthis._commonTarget = target;\n\t\t\tthis.poller.add(target);\n\t\t}\n\n\t\tif ((state === 'unwatchable') === (previous === 'unwatchable')) return;\n\n\t\tfor (const entry of this._sessions.values()) {\n\t\t\tif (entry.coversCommon) continue;\n\n\t\t\tif (!initial) {\n\t\t\t\tthis.deliverOverflow(entry);\n\t\t\t}\n\t\t\tentry.hooks.onUnwatchableChanged?.(entry.repoPath);\n\t\t}\n\t}\n\n\tprivate stopPolling(entry: SessionEntry): void {\n\t\tif (entry.target == null) return;\n\n\t\tthis.poller?.delete(entry.target);\n\t\tentry.target = undefined;\n\t}\n\n\t/**\n\t * A session rooted at the common dir polls the common files too, with the refs of every session in the group: its\n\t * combined watcher would hear them all. While the common dir is polled too, the poller stats each shared file once\n\t * for both, and each keeps its own readings, so what one watcher reported is held back from its own sessions only\n\t */\n\tprivate getRootProbes(entry: SessionEntry): readonly GitDirProbe[] {\n\t\tif (!entry.coversCommon) return gitDirRootProbes;\n\n\t\tconst refs = new Set<string>();\n\t\tfor (const e of this._sessions.values()) {\n\t\t\tfor (const ref of e.hooks.getProbeRefs?.() ?? []) {\n\t\t\t\trefs.add(ref);\n\t\t\t}\n\t\t}\n\t\treturn [...gitDirRootProbes, ...gitDirCommonProbes, ...getGitDirRefProbes(refs)];\n\t}\n\n\t/**\n\t * A linked worktree's `logs/HEAD` moved — a commit or reset there, on a branch a polled session rooted at the common\n\t * dir may not probe — so each such session hears a `heads` change its combined watcher would have heard\n\t */\n\tprivate onLinkedHeadMoved(): void {\n\t\tfor (const entry of this._sessions.values()) {\n\t\t\tif (!entry.coversCommon || entry.target == null) continue;\n\n\t\t\tentry.hooks.onRepoChanged(entry.repoPath, ['heads']);\n\t\t}\n\t}\n\n\t/** The common files, with the refs of every linked session (the only ones the common watcher delivers to) */\n\tprivate getCommonProbes(): GitDirProbe[] {\n\t\tconst refs = new Set<string>();\n\t\tfor (const entry of this._sessions.values()) {\n\t\t\tif (entry.coversCommon) continue;\n\n\t\t\tfor (const ref of entry.hooks.getProbeRefs?.() ?? []) {\n\t\t\t\trefs.add(ref);\n\t\t\t}\n\t\t}\n\t\treturn [...gitDirCommonProbes, ...getGitDirRefProbes(refs)];\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\tif (this.readFile != null && isConfigFile(relativePath)) {\n\t\t\tthis.onConfigReported();\n\t\t\treturn;\n\t\t}\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], undefined, true);\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\t// A linked worktree's git dir isn't the common one, so no file there is the shared config\n\t\tif (entry.coversCommon && this.readFile != null && isConfigFile(relativePath)) {\n\t\t\tthis.onConfigReported();\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\tprivate onConfigReported(): void {\n\t\t// The baseline may already hold the change reported, which comparing with it would lose\n\t\tif (this._configBaselining) {\n\t\t\tthis.deliverConfigChange(undefined);\n\t\t\treturn;\n\t\t}\n\t\tif (this._configReading) {\n\t\t\tthis._configRereadQueued = true;\n\t\t\treturn;\n\t\t}\n\n\t\tvoid this.readConfigChanges();\n\t}\n\n\tprivate async readConfigBaseline(): Promise<void> {\n\t\tthis._configBaselining = true;\n\t\tthis._configReading = true;\n\t\ttry {\n\t\t\tconst config = await this.readConfig();\n\t\t\tif (this._disposed) return;\n\n\t\t\tthis._config = config;\n\t\t} finally {\n\t\t\tthis._configBaselining = false;\n\t\t\tthis._configReading = false;\n\t\t}\n\t}\n\n\t/** Reads and compares until no report came in during the last read: it may describe a change the read missed */\n\tprivate async readConfigChanges(): Promise<void> {\n\t\tthis._configReading = true;\n\t\ttry {\n\t\t\tdo {\n\t\t\t\tthis._configRereadQueued = false;\n\t\t\t\tconst config = await this.readConfig();\n\t\t\t\tif (this._disposed) return;\n\n\t\t\t\tconst previous = this._config;\n\t\t\t\tif (config == null || previous == null) {\n\t\t\t\t\t// Unreadable now, or never readable before: the change may be anywhere. A failed read keeps the last\n\t\t\t\t\t// snapshot, so the next read still compares with what every session was told\n\t\t\t\t\tthis._config ??= config;\n\t\t\t\t\tthis.deliverConfigChange(undefined);\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\tthis._config = config;\n\t\t\t\tconst change = diffGitConfigSections(previous, config);\n\t\t\t\tif (change != null) {\n\t\t\t\t\tthis.deliverConfigChange(change);\n\t\t\t\t}\n\t\t\t} while (this._configRereadQueued);\n\t\t} finally {\n\t\t\tthis._configReading = false;\n\t\t}\n\t}\n\n\t/** `undefined` when the file couldn't be read or parsed, or the read didn't settle in time */\n\tprivate async readConfig(): Promise<GitConfigSections | undefined> {\n\t\tconst readFile = this.readFile;\n\t\tif (readFile == null) return undefined;\n\n\t\tlet timer: ReturnType<typeof setTimeout> | undefined;\n\t\ttry {\n\t\t\tconst data = await Promise.race([\n\t\t\t\treadFile(joinPaths(this.commonGitDir, 'config')),\n\t\t\t\tnew Promise<undefined>(resolve => {\n\t\t\t\t\ttimer = setTimeout(resolve, configReadTimeoutMs, undefined);\n\t\t\t\t}),\n\t\t\t]);\n\t\t\treturn data != null ? parseGitConfigBytes(data) : undefined;\n\t\t} catch {\n\t\t\treturn undefined;\n\t\t} finally {\n\t\t\tclearTimeout(timer);\n\t\t}\n\t}\n\n\t/**\n\t * Tells every session of a change to the shared `.git/config`: confined to some branches' sections, or (`undefined`,\n\t * or a change outside them) anywhere, which may have changed a remote's settings too. Either way no ref moved\n\t */\n\tprivate deliverConfigChange(change: GitConfigSectionsChange | undefined): void {\n\t\tfor (const entry of this._sessions.values()) {\n\t\t\tif (change?.confined) {\n\t\t\t\tentry.hooks.onRepoChanged(entry.repoPath, ['config'], false, false, change.branches);\n\t\t\t} else {\n\t\t\t\tentry.hooks.onRepoChanged(entry.repoPath, ['config', 'remotes'], false);\n\t\t\t}\n\t\t}\n\t}\n}\n\n/** The shared `.git/config` (or the lock git writes it through), relative to the common git dir */\nfunction isConfigFile(relativePath: string): boolean {\n\treturn relativePath === 'config' || relativePath === 'config.lock';\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. A lock file names the ref it locks; the only ones classification lets through are the pseudo-refs' and\n * `refs/stash.lock`.\n */\nfunction getChangedRef(relativePath: string): string | false | undefined {\n\tconst ref = relativePath.endsWith('.lock') ? relativePath.substring(0, relativePath.length - 5) : relativePath;\n\t// Remote config still invalidates remote and branch caches, but doesn't itself move any ref.\n\tif (ref === 'config' || ref === 'config.worktree') return false;\n\n\treturn ref.startsWith('refs/') || ref === 'HEAD' || ref === 'ORIG_HEAD' ? ref : undefined;\n}\n"],"mappings":"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"}
@@ -18,12 +18,20 @@ export declare class BranchesGitSubProvider implements GitBranchesSubProvider {
18
18
  /** Ahead/behind counts by `<baseSha>...<tipSha>` range, from the per-tip fallback; a pair of commits always counts the same */
19
19
  private readonly aheadBehindBySha;
20
20
  /**
21
- * For a base that moved only forward, the commits its reflog reaches off the new tip's history (else `false`), by
22
- * `<ref>\0<oldSha>\0<newSha>`, for each cached ref list it was read against: a ref write replaces that list, along
23
- * with the reflog the answer came from. `undefined` when git couldn't say, which isn't kept. A fetch moves one base
24
- * under every worktree at once, so they share the one check, joining it while it runs
21
+ * For a base that moved only forward, what its reflog reaches (else `false`), by `<ref>\0<oldSha>\0<newSha>`, for
22
+ * each cached ref list it was read against. A fetch moves one base under every worktree at once, so they share the
23
+ * one check, joining it while it runs
25
24
  */
26
25
  private readonly baseMovedForward;
26
+ /**
27
+ * For a base that moved, the count of the commits its new tip lacks for each local branch a worktree has checked
28
+ * out, by tip sha, keyed by that new tip alone for each cached ref list it was read against (which also fixes the
29
+ * branches counted). The counts don't depend on the ref or where it moved from, so each worktree the move reached
30
+ * finds its HEAD's count in the one read, even one whose previous answer was taken at another old tip. Only the
31
+ * checked-out branches are counted: the shared walk runs until every counted branch meets the new tip, so an
32
+ * unrelated old or orphan branch no worktree uses would make it deeper than any worktree's own count
33
+ */
34
+ private readonly aheadOfMovedBase;
27
35
  /**
28
36
  * When each `commonPath\0branch\0key` was last stamped by this process. Bounded: a dropped entry only costs a
29
37
  * read of the stored stamp, which still holds off a write under 5 minutes old
@@ -177,11 +185,12 @@ export declare class BranchesGitSubProvider implements GitBranchesSubProvider {
177
185
  private isRemoteHeadOf;
178
186
  /**
179
187
  * For `ref` (a full name in `records`, the cached ref list) moving from `oldTip` to `newTip`, the commits its reflog
180
- * reaches off `newTip`'s history when it moved only forward: `oldTip` and every value its reflog took after it are in
181
- * `newTip`'s history. `--fork-point` reads every reflog entry, so one in between that shares more of HEAD's history
182
- * than `newTip` does (a branch's commit that landed and was force-pushed away) moves the fork point while the count
183
- * stands; an older one off `newTip`'s history may hold such a commit too, which each worktree's count checks for.
184
- * Without a reflog it reads only the tip. `undefined` when it didn't move only forward, or git can't say.
188
+ * holds and those it reaches off `newTip`'s history, when it moved only forward: `oldTip` and every value its reflog
189
+ * took after it are in `newTip`'s history. `--fork-point` reads every reflog entry, so one in between that shares
190
+ * more of HEAD's history than `newTip` does (a branch's commit that landed and was force-pushed away) moves the fork
191
+ * point while the count stands; an older one off `newTip`'s history may hold such a commit too, which each
192
+ * worktree's count checks for. Without a reflog it reads only the tip. `undefined` when it didn't move only forward,
193
+ * or git can't say.
185
194
  */
186
195
  private getMovedForward;
187
196
  /**
@@ -191,6 +200,21 @@ export declare class BranchesGitSubProvider implements GitBranchesSubProvider {
191
200
  * `undefined` when git couldn't say.
192
201
  */
193
202
  private readMovedForward;
203
+ /**
204
+ * For a base moved to `newTip`, read against `records` (the cached ref list), the count of the commits `newTip`
205
+ * lacks for each local branch a worktree has checked out, by tip sha, so a detached HEAD at one of those tips finds
206
+ * its count too. Only those branches are counted: the one walk runs until every counted branch meets `newTip`, so an
207
+ * unrelated old or orphan branch would make it deeper than any worktree's own count, and a HEAD at a commit no
208
+ * checked-out branch points at isn't in the answer. The branches derive from `records`, which the read is kept
209
+ * per, so `newTip` alone keys it. `undefined` when git can't count them in one pass (before 2.41), there is no
210
+ * checked-out branch or `records` can't tell which are, or git couldn't say.
211
+ */
212
+ private getAheadOfMovedBase;
213
+ /**
214
+ * Reads {@link getAheadOfMovedBase} from git, in `commonPath`, which holds every worktree's branches. Not bound to
215
+ * one caller's cancellation, since every worktree the move reached rides this one read.
216
+ */
217
+ private readAheadOfMovedBase;
194
218
  /**
195
219
  * Resolves the base from the recorded candidates, then the branch it was created from or the default branch, for
196
220
  * {@link getAheadOfBase}
@@ -56,6 +56,39 @@ const branchActivityKeys = [
56
56
  * read costs less
57
57
  */
58
58
  const maxOffHistoryCommits = 64;
59
+ /** Reads a `%(ahead-behind:<base>)` field, `<ahead> <behind>`; `undefined` when it isn't one */
60
+ function parseAheadBehindField(field) {
61
+ const [ahead, behind] = (field ?? "").split(" ").map((n) => parseInt(n, 10));
62
+ if (isNaN(ahead) || isNaN(behind)) return void 0;
63
+ return {
64
+ ahead,
65
+ behind
66
+ };
67
+ }
68
+ /** The named refs as `for-each-ref` patterns, or `undefined` when they're too many to fit one command line */
69
+ function toRefPatterns(refs) {
70
+ return sum(refs, (r) => r.length + 1) < 15e3 ? [...refs] : void 0;
71
+ }
72
+ /**
73
+ * Joins the read `key` names against `records` (a cached ref list) in `memo`, or starts it with `read`. An answer is
74
+ * kept as long as that list is: a ref write replaces the list, along with the refs and reflog the answer came from.
75
+ * `undefined`, when git couldn't say, isn't kept
76
+ */
77
+ async function shareBaseMoveRead(memo, records, key, read) {
78
+ let reads = memo.get(records);
79
+ if (reads == null) {
80
+ reads = /* @__PURE__ */ new Map();
81
+ memo.set(records, reads);
82
+ }
83
+ let pending = reads.get(key);
84
+ if (pending == null) {
85
+ pending = read();
86
+ reads.set(key, pending);
87
+ }
88
+ const answer = await pending;
89
+ if (answer === void 0 && reads.get(key) === pending) reads.delete(key);
90
+ return answer;
91
+ }
59
92
  var BranchesGitSubProvider = class {
60
93
  context;
61
94
  git;
@@ -64,13 +97,21 @@ var BranchesGitSubProvider = class {
64
97
  /** Ahead/behind counts by `<baseSha>...<tipSha>` range, from the per-tip fallback; a pair of commits always counts the same */
65
98
  aheadBehindBySha = new LruMap(4096);
66
99
  /**
67
- * For a base that moved only forward, the commits its reflog reaches off the new tip's history (else `false`), by
68
- * `<ref>\0<oldSha>\0<newSha>`, for each cached ref list it was read against: a ref write replaces that list, along
69
- * with the reflog the answer came from. `undefined` when git couldn't say, which isn't kept. A fetch moves one base
70
- * under every worktree at once, so they share the one check, joining it while it runs
100
+ * For a base that moved only forward, what its reflog reaches (else `false`), by `<ref>\0<oldSha>\0<newSha>`, for
101
+ * each cached ref list it was read against. A fetch moves one base under every worktree at once, so they share the
102
+ * one check, joining it while it runs
71
103
  */
72
104
  baseMovedForward = /* @__PURE__ */ new WeakMap();
73
105
  /**
106
+ * For a base that moved, the count of the commits its new tip lacks for each local branch a worktree has checked
107
+ * out, by tip sha, keyed by that new tip alone for each cached ref list it was read against (which also fixes the
108
+ * branches counted). The counts don't depend on the ref or where it moved from, so each worktree the move reached
109
+ * finds its HEAD's count in the one read, even one whose previous answer was taken at another old tip. Only the
110
+ * checked-out branches are counted: the shared walk runs until every counted branch meets the new tip, so an
111
+ * unrelated old or orphan branch no worktree uses would make it deeper than any worktree's own count
112
+ */
113
+ aheadOfMovedBase = /* @__PURE__ */ new WeakMap();
114
+ /**
74
115
  * When each `commonPath\0branch\0key` was last stamped by this process. Bounded: a dropped entry only costs a
75
116
  * read of the stored stamp, which still holds off a write under 5 minutes old
76
117
  */
@@ -1246,17 +1287,24 @@ var BranchesGitSubProvider = class {
1246
1287
  if (tip === oldTip || tip === newTip) return void 0;
1247
1288
  }
1248
1289
  if (await this.isShallowRepository(repoPath) !== false) return void 0;
1249
- const offHistory = await this.getMovedForward(repoPath, records, record.name, oldTip, newTip);
1290
+ const movedForward = await this.getMovedForward(repoPath, records, record.name, oldTip, newTip);
1250
1291
  if (cancellation?.aborted) throw new CancellationError();
1251
- if (!offHistory) return void 0;
1252
- const result = await this.git.run({
1253
- cwd: repoPath,
1254
- cancellation,
1255
- errors: "ignore"
1256
- }, "rev-list", "--count", head, "--not", newTip, ...base.forkPoint != null ? [base.forkPoint] : [], ...offHistory, "--");
1292
+ if (!movedForward) return void 0;
1293
+ const { offHistory, logged } = movedForward;
1294
+ const aheadOfNewTip = (await this.getAheadOfMovedBase(repoPath, records, newTip))?.get(head);
1257
1295
  if (cancellation?.aborted) throw new CancellationError();
1258
- if (result.completion.status !== "exited" || result.exitCode !== 0) return void 0;
1259
- if (parseInt(result.stdout.trim(), 10) !== base.ahead) return void 0;
1296
+ if (aheadOfNewTip != null && aheadOfNewTip !== base.ahead) return void 0;
1297
+ const forkPoint = base.forkPoint;
1298
+ if (aheadOfNewTip == null || offHistory.length !== 0 || forkPoint != null && forkPoint !== oldTip && forkPoint !== newTip && !logged.has(forkPoint)) {
1299
+ const result = await this.git.run({
1300
+ cwd: repoPath,
1301
+ cancellation,
1302
+ errors: "ignore"
1303
+ }, "rev-list", "--count", head, "--not", newTip, ...forkPoint != null ? [forkPoint] : [], ...offHistory, "--");
1304
+ if (cancellation?.aborted) throw new CancellationError();
1305
+ if (result.completion.status !== "exited" || result.exitCode !== 0) return void 0;
1306
+ if (parseInt(result.stdout.trim(), 10) !== base.ahead) return void 0;
1307
+ }
1260
1308
  return withDefaultBranch({
1261
1309
  base: {
1262
1310
  ...base,
@@ -1291,27 +1339,15 @@ var BranchesGitSubProvider = class {
1291
1339
  }
1292
1340
  /**
1293
1341
  * For `ref` (a full name in `records`, the cached ref list) moving from `oldTip` to `newTip`, the commits its reflog
1294
- * reaches off `newTip`'s history when it moved only forward: `oldTip` and every value its reflog took after it are in
1295
- * `newTip`'s history. `--fork-point` reads every reflog entry, so one in between that shares more of HEAD's history
1296
- * than `newTip` does (a branch's commit that landed and was force-pushed away) moves the fork point while the count
1297
- * stands; an older one off `newTip`'s history may hold such a commit too, which each worktree's count checks for.
1298
- * Without a reflog it reads only the tip. `undefined` when it didn't move only forward, or git can't say.
1342
+ * holds and those it reaches off `newTip`'s history, when it moved only forward: `oldTip` and every value its reflog
1343
+ * took after it are in `newTip`'s history. `--fork-point` reads every reflog entry, so one in between that shares
1344
+ * more of HEAD's history than `newTip` does (a branch's commit that landed and was force-pushed away) moves the fork
1345
+ * point while the count stands; an older one off `newTip`'s history may hold such a commit too, which each
1346
+ * worktree's count checks for. Without a reflog it reads only the tip. `undefined` when it didn't move only forward,
1347
+ * or git can't say.
1299
1348
  */
1300
1349
  async getMovedForward(repoPath, records, ref, oldTip, newTip) {
1301
- let checks = this.baseMovedForward.get(records);
1302
- if (checks == null) {
1303
- checks = /* @__PURE__ */ new Map();
1304
- this.baseMovedForward.set(records, checks);
1305
- }
1306
- const key = `${ref}\0${oldTip}\0${newTip}`;
1307
- let check = checks.get(key);
1308
- if (check == null) {
1309
- check = this.readMovedForward(this.cache.getCommonPath(repoPath), ref, oldTip, newTip);
1310
- checks.set(key, check);
1311
- }
1312
- const offHistory = await check;
1313
- if (offHistory == null && checks.get(key) === check) checks.delete(key);
1314
- return offHistory || void 0;
1350
+ return await shareBaseMoveRead(this.baseMovedForward, records, `${ref}\0${oldTip}\0${newTip}`, () => this.readMovedForward(this.cache.getCommonPath(repoPath), ref, oldTip, newTip)) || void 0;
1315
1351
  }
1316
1352
  /**
1317
1353
  * Reads {@link getMovedForward} from git, in `commonPath`, which holds the refs, their reflogs and the objects for
@@ -1356,7 +1392,58 @@ var BranchesGitSubProvider = class {
1356
1392
  const off = result.stdout.split("\n").filter((sha) => sha.length > 0);
1357
1393
  if (off.length > maxOffHistoryCommits) return false;
1358
1394
  if (off.some((sha) => since.has(sha))) return false;
1359
- return off;
1395
+ return {
1396
+ offHistory: off,
1397
+ logged
1398
+ };
1399
+ } catch {
1400
+ return;
1401
+ }
1402
+ }
1403
+ /**
1404
+ * For a base moved to `newTip`, read against `records` (the cached ref list), the count of the commits `newTip`
1405
+ * lacks for each local branch a worktree has checked out, by tip sha, so a detached HEAD at one of those tips finds
1406
+ * its count too. Only those branches are counted: the one walk runs until every counted branch meets `newTip`, so an
1407
+ * unrelated old or orphan branch would make it deeper than any worktree's own count, and a HEAD at a commit no
1408
+ * checked-out branch points at isn't in the answer. The branches derive from `records`, which the read is kept
1409
+ * per, so `newTip` alone keys it. `undefined` when git can't count them in one pass (before 2.41), there is no
1410
+ * checked-out branch or `records` can't tell which are, or git couldn't say.
1411
+ */
1412
+ getAheadOfMovedBase(repoPath, records, newTip) {
1413
+ return shareBaseMoveRead(this.aheadOfMovedBase, records, newTip, () => this.readAheadOfMovedBase(this.cache.getCommonPath(repoPath), records, newTip));
1414
+ }
1415
+ /**
1416
+ * Reads {@link getAheadOfMovedBase} from git, in `commonPath`, which holds every worktree's branches. Not bound to
1417
+ * one caller's cancellation, since every worktree the move reached rides this one read.
1418
+ */
1419
+ async readAheadOfMovedBase(commonPath, records, newTip) {
1420
+ try {
1421
+ if (!await this.git.supports("git:for-each-ref:ahead-behind")) return void 0;
1422
+ const checkedOut = /* @__PURE__ */ new Set();
1423
+ for (const record of records) {
1424
+ if (!record.name.startsWith("refs/heads/")) continue;
1425
+ if (record.worktreePath == null) return void 0;
1426
+ if (record.worktreePath) checkedOut.add(record.name);
1427
+ }
1428
+ if (!checkedOut.size) return void 0;
1429
+ const patterns = toRefPatterns(checkedOut) ?? ["refs/heads"];
1430
+ const result = await this.git.run({
1431
+ cwd: commonPath,
1432
+ errors: "ignore"
1433
+ }, "for-each-ref", `--format=%(refname)%09%(objectname)%09%(ahead-behind:${newTip})`, ...patterns);
1434
+ this.provider.maintenance?.request(commonPath, "commit-graph");
1435
+ if (result.completion.status !== "exited" || result.exitCode !== 0) return void 0;
1436
+ const aheadByTip = /* @__PURE__ */ new Map();
1437
+ for (const line of result.stdout.split("\n")) {
1438
+ if (!line) continue;
1439
+ const [ref, sha, field] = line.split(" ");
1440
+ if (!ref || !sha) return void 0;
1441
+ if (!checkedOut.has(ref)) continue;
1442
+ const counted = parseAheadBehindField(field);
1443
+ if (counted == null) continue;
1444
+ aheadByTip.set(sha, counted.ahead);
1445
+ }
1446
+ return aheadByTip;
1360
1447
  } catch {
1361
1448
  return;
1362
1449
  }
@@ -1511,8 +1598,7 @@ var BranchesGitSubProvider = class {
1511
1598
  if (refs?.size === 0) return counts;
1512
1599
  let patterns;
1513
1600
  if (refs == null) patterns = options?.remotes ? ["refs/heads", "refs/remotes"] : ["refs/heads"];
1514
- else if (sum(refs, (r) => r.length + 1) < 15e3) patterns = [...refs];
1515
- else patterns = ["refs/heads", "refs/remotes"];
1601
+ else patterns = toRefPatterns(refs) ?? ["refs/heads", "refs/remotes"];
1516
1602
  if (!base.includes(")") && await this.git.supports("git:for-each-ref:ahead-behind")) {
1517
1603
  const result = await this.git.run({
1518
1604
  cwd: repoPath,
@@ -1524,12 +1610,9 @@ var BranchesGitSubProvider = class {
1524
1610
  if (!line) continue;
1525
1611
  const [ref, symref, counted] = line.split(" ");
1526
1612
  if (symref || refs != null && !refs.has(ref)) continue;
1527
- const [ahead, behind] = (counted ?? "").split(" ").map((n) => parseInt(n, 10));
1528
- if (isNaN(ahead) || isNaN(behind)) throw new Error(`Unexpected output counting the commits ahead of and behind '${base}': ${line}`);
1529
- counts.set(ref, {
1530
- ahead,
1531
- behind
1532
- });
1613
+ const aheadBehind = parseAheadBehindField(counted);
1614
+ if (aheadBehind == null) throw new Error(`Unexpected output counting the commits ahead of and behind '${base}': ${line}`);
1615
+ counts.set(ref, aheadBehind);
1533
1616
  }
1534
1617
  return counts;
1535
1618
  }