@gitkraken/core-gitlens 0.5.113 → 0.5.114

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 (241) hide show
  1. package/CHANGELOG.md +17 -1
  2. package/dist/git/cache.d.ts +4 -0
  3. package/dist/git/cache.d.ts.map +1 -1
  4. package/dist/git/cache.js +33 -0
  5. package/dist/git/cache.js.map +1 -1
  6. package/dist/git/context.d.ts +10 -0
  7. package/dist/git/context.d.ts.map +1 -1
  8. package/dist/git/context.js.map +1 -1
  9. package/dist/git/errors.d.ts +6 -3
  10. package/dist/git/errors.d.ts.map +1 -1
  11. package/dist/git/errors.js +9 -1
  12. package/dist/git/errors.js.map +1 -1
  13. package/dist/git/features.d.ts +1 -1
  14. package/dist/git/features.d.ts.map +1 -1
  15. package/dist/git/features.js +38 -0
  16. package/dist/git/features.js.map +1 -1
  17. package/dist/git/gitHealth.d.ts +153 -0
  18. package/dist/git/gitHealth.d.ts.map +1 -0
  19. package/dist/git/gitHealth.js +353 -0
  20. package/dist/git/gitHealth.js.map +1 -0
  21. package/dist/git/models/search.d.ts +6 -3
  22. package/dist/git/models/search.d.ts.map +1 -1
  23. package/dist/git/models/search.js +5 -1
  24. package/dist/git/models/search.js.map +1 -1
  25. package/dist/git/parsers/diffParser.d.ts +8 -2
  26. package/dist/git/parsers/diffParser.d.ts.map +1 -1
  27. package/dist/git/parsers/diffParser.js +27 -4
  28. package/dist/git/parsers/diffParser.js.map +1 -1
  29. package/dist/git/providers/config.d.ts +36 -3
  30. package/dist/git/providers/config.d.ts.map +1 -1
  31. package/dist/git/providers/graph.d.ts +9 -0
  32. package/dist/git/providers/graph.d.ts.map +1 -1
  33. package/dist/git/providers/maintenance.d.ts +222 -0
  34. package/dist/git/providers/maintenance.d.ts.map +1 -0
  35. package/dist/git/providers/maintenance.js +20 -0
  36. package/dist/git/providers/maintenance.js.map +1 -0
  37. package/dist/git/providers/operations.d.ts +10 -0
  38. package/dist/git/providers/operations.d.ts.map +1 -1
  39. package/dist/git/providers/pausedOperations.d.ts +3 -0
  40. package/dist/git/providers/pausedOperations.d.ts.map +1 -1
  41. package/dist/git/providers/provider.d.ts +2 -0
  42. package/dist/git/providers/provider.d.ts.map +1 -1
  43. package/dist/git/providers/tags.d.ts +3 -0
  44. package/dist/git/providers/tags.d.ts.map +1 -1
  45. package/dist/git/repositoryService.d.ts +1 -0
  46. package/dist/git/repositoryService.d.ts.map +1 -1
  47. package/dist/git/repositoryService.js.map +1 -1
  48. package/dist/git/run.types.d.ts +8 -0
  49. package/dist/git/run.types.d.ts.map +1 -1
  50. package/dist/git/service.js +3 -0
  51. package/dist/git/service.js.map +1 -1
  52. package/dist/git/utils/search.utils.d.ts.map +1 -1
  53. package/dist/git/utils/search.utils.js +112 -5
  54. package/dist/git/utils/search.utils.js.map +1 -1
  55. package/dist/git/watching/watchService.d.ts +3 -0
  56. package/dist/git/watching/watchService.d.ts.map +1 -1
  57. package/dist/git/watching/watchService.js +8 -0
  58. package/dist/git/watching/watchService.js.map +1 -1
  59. package/dist/git-cli/cliGitProvider.d.ts +11 -0
  60. package/dist/git-cli/cliGitProvider.d.ts.map +1 -1
  61. package/dist/git-cli/cliGitProvider.js +12 -0
  62. package/dist/git-cli/cliGitProvider.js.map +1 -1
  63. package/dist/git-cli/exec/git.d.ts +23 -1
  64. package/dist/git-cli/exec/git.d.ts.map +1 -1
  65. package/dist/git-cli/exec/git.js +83 -8
  66. package/dist/git-cli/exec/git.js.map +1 -1
  67. package/dist/git-cli/exec/gitQueue.d.ts +2 -0
  68. package/dist/git-cli/exec/gitQueue.d.ts.map +1 -1
  69. package/dist/git-cli/exec/gitQueue.js +19 -17
  70. package/dist/git-cli/exec/gitQueue.js.map +1 -1
  71. package/dist/git-cli/parsers/logParser.d.ts.map +1 -1
  72. package/dist/git-cli/parsers/logParser.js +4 -1
  73. package/dist/git-cli/parsers/logParser.js.map +1 -1
  74. package/dist/git-cli/providers/blame.d.ts.map +1 -1
  75. package/dist/git-cli/providers/blame.js +4 -7
  76. package/dist/git-cli/providers/blame.js.map +1 -1
  77. package/dist/git-cli/providers/branches.d.ts.map +1 -1
  78. package/dist/git-cli/providers/branches.js +13 -3
  79. package/dist/git-cli/providers/branches.js.map +1 -1
  80. package/dist/git-cli/providers/commits.d.ts.map +1 -1
  81. package/dist/git-cli/providers/commits.js +34 -3
  82. package/dist/git-cli/providers/commits.js.map +1 -1
  83. package/dist/git-cli/providers/config.d.ts +27 -0
  84. package/dist/git-cli/providers/config.d.ts.map +1 -1
  85. package/dist/git-cli/providers/config.js +31 -6
  86. package/dist/git-cli/providers/config.js.map +1 -1
  87. package/dist/git-cli/providers/graph.d.ts +8 -0
  88. package/dist/git-cli/providers/graph.d.ts.map +1 -1
  89. package/dist/git-cli/providers/graph.js +85 -6
  90. package/dist/git-cli/providers/graph.js.map +1 -1
  91. package/dist/git-cli/providers/maintenance.d.ts +306 -0
  92. package/dist/git-cli/providers/maintenance.d.ts.map +1 -0
  93. package/dist/git-cli/providers/maintenance.js +2129 -0
  94. package/dist/git-cli/providers/maintenance.js.map +1 -0
  95. package/dist/git-cli/providers/operations.d.ts +1 -0
  96. package/dist/git-cli/providers/operations.d.ts.map +1 -1
  97. package/dist/git-cli/providers/operations.js +8 -0
  98. package/dist/git-cli/providers/operations.js.map +1 -1
  99. package/dist/git-cli/providers/pausedOperations.d.ts +1 -0
  100. package/dist/git-cli/providers/pausedOperations.d.ts.map +1 -1
  101. package/dist/git-cli/providers/pausedOperations.js +1 -1
  102. package/dist/git-cli/providers/pausedOperations.js.map +1 -1
  103. package/dist/git-cli/providers/tags.d.ts +3 -0
  104. package/dist/git-cli/providers/tags.d.ts.map +1 -1
  105. package/dist/git-cli/providers/tags.js +23 -1
  106. package/dist/git-cli/providers/tags.js.map +1 -1
  107. package/dist/plus/agents/agentCapabilities.d.ts +77 -0
  108. package/dist/plus/agents/agentCapabilities.d.ts.map +1 -0
  109. package/dist/plus/agents/agentCapabilities.js +197 -0
  110. package/dist/plus/agents/agentCapabilities.js.map +1 -0
  111. package/dist/plus/agents/providers/claudeCodeTranscript.d.ts +77 -13
  112. package/dist/plus/agents/providers/claudeCodeTranscript.d.ts.map +1 -1
  113. package/dist/plus/agents/providers/claudeCodeTranscript.js +263 -55
  114. package/dist/plus/agents/providers/claudeCodeTranscript.js.map +1 -1
  115. package/dist/plus/agents/providers/{claudeCodeProvider.d.ts → gkAgentProvider.d.ts} +52 -19
  116. package/dist/plus/agents/providers/gkAgentProvider.d.ts.map +1 -0
  117. package/dist/plus/agents/providers/{claudeCodeProvider.js → gkAgentProvider.js} +935 -262
  118. package/dist/plus/agents/providers/gkAgentProvider.js.map +1 -0
  119. package/dist/plus/agents/types.d.ts +113 -35
  120. package/dist/plus/agents/types.d.ts.map +1 -1
  121. package/dist/plus/agents/types.js +7 -4
  122. package/dist/plus/agents/types.js.map +1 -1
  123. package/dist/plus/ai/models/promptTemplates.d.ts +1 -1
  124. package/dist/plus/ai/models/promptTemplates.d.ts.map +1 -1
  125. package/dist/plus/ai/prompts.d.ts +4 -0
  126. package/dist/plus/ai/prompts.d.ts.map +1 -1
  127. package/dist/plus/ai/prompts.js +59 -4
  128. package/dist/plus/ai/prompts.js.map +1 -1
  129. package/dist/plus/git-github/api/github.d.ts +44 -1
  130. package/dist/plus/git-github/api/github.d.ts.map +1 -1
  131. package/dist/plus/git-github/api/github.js +318 -25
  132. package/dist/plus/git-github/api/github.js.map +1 -1
  133. package/dist/plus/git-github/api/issueSearchQuery.d.ts +13 -0
  134. package/dist/plus/git-github/api/issueSearchQuery.d.ts.map +1 -1
  135. package/dist/plus/git-github/api/issueSearchQuery.js +41 -0
  136. package/dist/plus/git-github/api/issueSearchQuery.js.map +1 -1
  137. package/dist/plus/git-github/providers/github/blame.d.ts.map +1 -1
  138. package/dist/plus/git-github/providers/github/blame.js +2 -1
  139. package/dist/plus/git-github/providers/github/blame.js.map +1 -1
  140. package/dist/plus/integrations/integrationService.d.ts +11 -0
  141. package/dist/plus/integrations/integrationService.d.ts.map +1 -1
  142. package/dist/plus/integrations/integrationService.js +4 -0
  143. package/dist/plus/integrations/integrationService.js.map +1 -1
  144. package/dist/plus/integrations/manager.d.ts +26 -0
  145. package/dist/plus/integrations/manager.d.ts.map +1 -1
  146. package/dist/plus/integrations/models/gitHostIntegration.d.ts +31 -0
  147. package/dist/plus/integrations/models/gitHostIntegration.d.ts.map +1 -1
  148. package/dist/plus/integrations/models/gitHostIntegration.js +31 -0
  149. package/dist/plus/integrations/models/gitHostIntegration.js.map +1 -1
  150. package/dist/plus/integrations/models/integration.d.ts +1 -1
  151. package/dist/plus/integrations/models/integration.d.ts.map +1 -1
  152. package/dist/plus/integrations/models/integration.js.map +1 -1
  153. package/dist/plus/integrations/providers/github.d.ts +9 -0
  154. package/dist/plus/integrations/providers/github.d.ts.map +1 -1
  155. package/dist/plus/integrations/providers/github.js +7 -0
  156. package/dist/plus/integrations/providers/github.js.map +1 -1
  157. package/dist/plus/integrations/reads/broaden.d.ts +47 -10
  158. package/dist/plus/integrations/reads/broaden.d.ts.map +1 -1
  159. package/dist/plus/integrations/reads/broaden.js +144 -33
  160. package/dist/plus/integrations/reads/broaden.js.map +1 -1
  161. package/dist/plus/integrations/reads/filters.d.ts +18 -0
  162. package/dist/plus/integrations/reads/filters.d.ts.map +1 -1
  163. package/dist/plus/integrations/reads/filters.js +37 -1
  164. package/dist/plus/integrations/reads/filters.js.map +1 -1
  165. package/dist/plus/integrations/reads/issueBatch.d.ts +51 -0
  166. package/dist/plus/integrations/reads/issueBatch.d.ts.map +1 -0
  167. package/dist/plus/integrations/reads/issueBatch.js +91 -0
  168. package/dist/plus/integrations/reads/issueBatch.js.map +1 -0
  169. package/dist/plus/integrations/reads/sweeps.d.ts.map +1 -1
  170. package/dist/plus/integrations/reads/sweeps.js +5 -3
  171. package/dist/plus/integrations/reads/sweeps.js.map +1 -1
  172. package/dist/utils/promiseCache.d.ts +19 -0
  173. package/dist/utils/promiseCache.d.ts.map +1 -1
  174. package/dist/utils/promiseCache.js +30 -0
  175. package/dist/utils/promiseCache.js.map +1 -1
  176. package/dist/utils/resourceUsage.d.ts +5 -0
  177. package/dist/utils/resourceUsage.d.ts.map +1 -0
  178. package/dist/utils/resourceUsage.js +2 -0
  179. package/dist/utils/resourceUsage.js.map +1 -0
  180. package/dist/utils/string.d.ts +12 -0
  181. package/dist/utils/string.d.ts.map +1 -1
  182. package/dist/utils/string.js +36 -1
  183. package/dist/utils/string.js.map +1 -1
  184. package/docs/integrations.md +29 -25
  185. package/docs/kepler-read-api-parity.md +12 -4
  186. package/package.json +11 -3
  187. package/src/git/cache.ts +36 -0
  188. package/src/git/context.ts +11 -0
  189. package/src/git/errors.ts +13 -1
  190. package/src/git/features.ts +51 -0
  191. package/src/git/gitHealth.ts +522 -0
  192. package/src/git/models/search.ts +19 -4
  193. package/src/git/parsers/diffParser.ts +27 -4
  194. package/src/git/providers/config.ts +42 -1
  195. package/src/git/providers/graph.ts +12 -0
  196. package/src/git/providers/maintenance.ts +224 -0
  197. package/src/git/providers/operations.ts +10 -0
  198. package/src/git/providers/pausedOperations.ts +3 -0
  199. package/src/git/providers/provider.ts +2 -0
  200. package/src/git/providers/tags.ts +1 -0
  201. package/src/git/repositoryService.ts +1 -0
  202. package/src/git/run.types.ts +8 -0
  203. package/src/git/service.ts +3 -0
  204. package/src/git/utils/search.utils.ts +129 -5
  205. package/src/git/watching/watchService.ts +10 -0
  206. package/src/git-cli/cliGitProvider.ts +25 -0
  207. package/src/git-cli/exec/git.ts +120 -6
  208. package/src/git-cli/exec/gitQueue.ts +19 -17
  209. package/src/git-cli/parsers/logParser.ts +4 -1
  210. package/src/git-cli/providers/blame.ts +3 -8
  211. package/src/git-cli/providers/branches.ts +28 -8
  212. package/src/git-cli/providers/commits.ts +34 -2
  213. package/src/git-cli/providers/config.ts +38 -6
  214. package/src/git-cli/providers/graph.ts +107 -7
  215. package/src/git-cli/providers/maintenance.ts +2599 -0
  216. package/src/git-cli/providers/operations.ts +11 -0
  217. package/src/git-cli/providers/pausedOperations.ts +7 -2
  218. package/src/git-cli/providers/tags.ts +36 -1
  219. package/src/plus/agents/agentCapabilities.ts +286 -0
  220. package/src/plus/agents/providers/claudeCodeTranscript.ts +313 -58
  221. package/src/plus/agents/providers/{claudeCodeProvider.ts → gkAgentProvider.ts} +1123 -277
  222. package/src/plus/agents/types.ts +137 -40
  223. package/src/plus/ai/models/promptTemplates.ts +1 -1
  224. package/src/plus/ai/prompts.ts +62 -4
  225. package/src/plus/git-github/api/github.ts +353 -24
  226. package/src/plus/git-github/api/issueSearchQuery.ts +49 -1
  227. package/src/plus/git-github/providers/github/blame.ts +2 -1
  228. package/src/plus/integrations/integrationService.ts +15 -0
  229. package/src/plus/integrations/manager.ts +26 -0
  230. package/src/plus/integrations/models/gitHostIntegration.ts +48 -0
  231. package/src/plus/integrations/models/integration.ts +1 -0
  232. package/src/plus/integrations/providers/github.ts +18 -0
  233. package/src/plus/integrations/reads/broaden.ts +282 -70
  234. package/src/plus/integrations/reads/filters.ts +39 -1
  235. package/src/plus/integrations/reads/issueBatch.ts +197 -0
  236. package/src/plus/integrations/reads/sweeps.ts +19 -12
  237. package/src/utils/promiseCache.ts +43 -0
  238. package/src/utils/resourceUsage.ts +5 -0
  239. package/src/utils/string.ts +38 -1
  240. package/dist/plus/agents/providers/claudeCodeProvider.d.ts.map +0 -1
  241. package/dist/plus/agents/providers/claudeCodeProvider.js.map +0 -1
@@ -0,0 +1,522 @@
1
+ import type {
2
+ GitHealthSnapshot,
3
+ GitMaintenanceTask,
4
+ GitOptimizationCapability,
5
+ GitOptimizationId,
6
+ } from './providers/maintenance.js';
7
+
8
+ /**
9
+ * Pure recommendation engine for the Git Health feature — no vscode imports, trivially unit-testable.
10
+ * Thresholds are initial, tunable, telemetry-informed values (see `.work/dev/git-health/spec.md`).
11
+ */
12
+
13
+ /** Working-tree size (via the index-bytes proxy) at/above which untracked-cache/fsmonitor/manyFiles help. */
14
+ export const trackedFilesThreshold = 10_000;
15
+ /** Loose-object estimate at/above which the `loose-objects` maintenance task is worth running. */
16
+ export const looseObjectsThreshold = 5_000;
17
+ /** Git's default uncovered-pack threshold for the `incremental-repack` auto condition. */
18
+ export const packsOutsideMultiPackIndexThreshold = 10;
19
+ /** Loose refs at/above which packing the files ref backend avoids repeated filesystem traversal. */
20
+ export const looseRefsThreshold = 256;
21
+ /** Total pack bytes at/above which a repo counts as "clearly large" for the banner gate (~1 GiB). */
22
+ export const largePackBytesThreshold = 1024 ** 3;
23
+ /** Rough bytes-per-entry of a git v2 index — turns `.git/index` size into a tracked-file-count proxy. */
24
+ export const approxBytesPerIndexEntry = 80;
25
+ /** Duration (ms) above which a git command is "slow" — mirrors the exec-layer slow-call threshold. */
26
+ export const slowCommandThresholdMs = 2000;
27
+
28
+ /** Git operation families whose runtime maps to a distinct repository optimization surface. */
29
+ export type GitHealthSlownessCategory = 'worktree' | 'history' | 'refs' | 'objects';
30
+
31
+ /** Persisted passive-slowness sample for one operation family. */
32
+ export interface GitHealthSlownessSample {
33
+ /** Number of slow git commands observed for this repo. */
34
+ readonly count: number;
35
+ /** Epoch ms of the most recent slow command. */
36
+ readonly lastAt: number;
37
+ /** Longest single slow-command duration observed (ms). */
38
+ readonly maxDurationMs: number;
39
+ }
40
+
41
+ /** Per-repo passive-slowness summary, split so recommendations only target the affected operation family. */
42
+ export type GitHealthSlowness = Partial<Record<GitHealthSlownessCategory, GitHealthSlownessSample>>;
43
+
44
+ export type GitOptimizationTier = 'auto' | 'ask';
45
+
46
+ /** Coarse duration buckets for maintenance telemetry. */
47
+ export type GitHealthDurationBucket = '<1s' | '1-5s' | '5-15s' | '15-60s' | '>60s';
48
+
49
+ /** Which measured signal tripped a finding (drives view copy + telemetry buckets). */
50
+ export type GitHealthFindingReason =
51
+ | 'looseObjects'
52
+ | 'looseRefs'
53
+ | 'packsOutsideMultiPackIndex'
54
+ | 'trackedFiles'
55
+ | 'largePacks'
56
+ | 'worktreeSlowness';
57
+
58
+ /** A single recommendation: a lever to apply, its tier, and the evidence that triggered it. */
59
+ export interface GitHealthFinding {
60
+ readonly tier: GitOptimizationTier;
61
+ readonly reason: GitHealthFindingReason;
62
+ /** The measured value that crossed the threshold. */
63
+ readonly value: number;
64
+ /** The threshold it crossed. */
65
+ readonly threshold: number;
66
+ /** The concrete action this finding recommends. */
67
+ readonly action:
68
+ | { readonly kind: 'maintenance'; readonly task: GitMaintenanceTask }
69
+ | { readonly kind: 'optimization'; readonly id: GitOptimizationId };
70
+ }
71
+
72
+ export interface GitHealthReport {
73
+ readonly findings: readonly GitHealthFinding[];
74
+ /** Repository shape that scopes local history, object, and working-tree measurements. */
75
+ readonly repository: GitHealthSnapshot['repository'];
76
+ /** Whether the repo is "clearly large" (any working-tree threshold met, or pack bytes ≥ ~1 GiB). */
77
+ readonly clearlyLarge: boolean;
78
+ /** Extrapolated loose-object count (from the probe sample). */
79
+ readonly estimatedLooseObjects: number;
80
+ /** Loose refs found by the bounded files-backend probe. */
81
+ readonly looseRefs: { readonly count: number; readonly exact: boolean };
82
+ /** Repository-size signal from the index; see {@link trackedFilesScope} for how to interpret it. */
83
+ readonly estimatedTrackedFiles: number;
84
+ /** `true` when {@link estimatedTrackedFiles} is the exact repository-wide count from a normal index. */
85
+ readonly trackedFilesExact: boolean;
86
+ /** Whether the displayed count covers the repository, a sparse working set, or is only a byte-size estimate. */
87
+ readonly trackedFilesScope: 'repository' | 'sparseWorkingTree' | 'estimate';
88
+ readonly packCount: number;
89
+ readonly packsOutsideMultiPackIndex: number | undefined;
90
+ readonly packBytes: number;
91
+ readonly commitGraph: {
92
+ readonly present: boolean;
93
+ readonly mtime: number | undefined;
94
+ readonly changedPaths: boolean;
95
+ readonly changedPathsSupported: boolean;
96
+ readonly disabled: boolean;
97
+ /** `core.commitGraph` explicitly false in git config — git won't read the cache, GitLens won't write it. */
98
+ readonly readDisabled: boolean;
99
+ };
100
+ }
101
+
102
+ /**
103
+ * Extrapolates total loose-object count from a uniform sample of the 256 object fanout dirs. Loose
104
+ * objects distribute uniformly by sha prefix, so `(objectsInSampled / dirsSampled) * 256` is sound.
105
+ */
106
+ export function estimateLooseObjects(objectsInSampled: number, dirsSampled: number): number {
107
+ if (dirsSampled <= 0) return 0;
108
+
109
+ return Math.round((objectsInSampled / dirsSampled) * 256);
110
+ }
111
+
112
+ /** Turns `.git/index` byte size into an approximate tracked-file count. */
113
+ export function estimateTrackedFiles(indexBytes: number): number {
114
+ if (indexBytes <= 0) return 0;
115
+
116
+ return Math.round(indexBytes / approxBytesPerIndexEntry);
117
+ }
118
+
119
+ /**
120
+ * Computes the {@link GitHealthReport} from a cheap probe, passive slowness, and capabilities. Findings
121
+ * are only emitted for levers that are supported, not already applied, and past their threshold.
122
+ */
123
+ export function computeHealthReport(
124
+ snapshot: GitHealthSnapshot,
125
+ slowness: GitHealthSlowness | undefined,
126
+ capabilities: readonly GitOptimizationCapability[],
127
+ ): GitHealthReport {
128
+ const estimatedLooseObjects = estimateLooseObjects(
129
+ snapshot.looseObjects.objectsInSampledDirs,
130
+ snapshot.looseObjects.dirsSampled,
131
+ );
132
+ // A normal index header is the exact tracked-file count. A sparse index header is still a useful exact
133
+ // count of its populated working set, but not of the whole repository. A split index header covers only
134
+ // the mutable layer and conflict stages duplicate paths, so neither raw count is used.
135
+ const useIndexEntryCount =
136
+ snapshot.indexEntryCount != null &&
137
+ (snapshot.indexEntryCountType === 'full' || snapshot.indexEntryCountType === 'sparse');
138
+ const estimatedTrackedFiles = useIndexEntryCount
139
+ ? snapshot.indexEntryCount
140
+ : estimateTrackedFiles(snapshot.indexBytes);
141
+ const trackedFilesExact = useIndexEntryCount && snapshot.indexEntryCountType === 'full';
142
+ const trackedFilesScope = trackedFilesExact
143
+ ? 'repository'
144
+ : useIndexEntryCount && snapshot.indexEntryCountType === 'sparse'
145
+ ? 'sparseWorkingTree'
146
+ : 'estimate';
147
+ const largeWorkingTree = estimatedTrackedFiles >= trackedFilesThreshold;
148
+ const clearlyLarge = largeWorkingTree || snapshot.packBytes >= largePackBytesThreshold;
149
+
150
+ const supported = (id: GitOptimizationId): boolean => capabilities.find(c => c.id === id)?.supported === true;
151
+
152
+ const findings: GitHealthFinding[] = [];
153
+
154
+ // Auto tier — safe `git maintenance run` one-shots (gated on `git maintenance` support).
155
+ if (snapshot.supportsMaintenanceRun) {
156
+ if (estimatedLooseObjects >= looseObjectsThreshold) {
157
+ findings.push({
158
+ tier: 'auto',
159
+ reason: 'looseObjects',
160
+ value: estimatedLooseObjects,
161
+ threshold: looseObjectsThreshold,
162
+ action: { kind: 'maintenance', task: 'loose-objects' },
163
+ });
164
+ }
165
+ const incrementalRepackThreshold = snapshot.incrementalRepackAutoThreshold;
166
+ const incrementalRepackDue =
167
+ snapshot.multiPackIndexEnabled === true &&
168
+ snapshot.packsOutsideMultiPackIndex != null &&
169
+ incrementalRepackThreshold != null &&
170
+ incrementalRepackThreshold !== 0 &&
171
+ (incrementalRepackThreshold < 0
172
+ ? snapshot.packCount > 0
173
+ : snapshot.packsOutsideMultiPackIndex >= incrementalRepackThreshold);
174
+ if (incrementalRepackDue) {
175
+ findings.push({
176
+ tier: 'auto',
177
+ reason: 'packsOutsideMultiPackIndex',
178
+ value: snapshot.packsOutsideMultiPackIndex,
179
+ threshold: incrementalRepackThreshold,
180
+ action: { kind: 'maintenance', task: 'incremental-repack' },
181
+ });
182
+ }
183
+ }
184
+ if (
185
+ snapshot.supportsPackRefsMaintenance &&
186
+ snapshot.repository.refFormat === 'files' &&
187
+ snapshot.looseRefs.count >= looseRefsThreshold
188
+ ) {
189
+ findings.push({
190
+ tier: 'auto',
191
+ reason: 'looseRefs',
192
+ value: snapshot.looseRefs.count,
193
+ threshold: looseRefsThreshold,
194
+ action: { kind: 'maintenance', task: 'pack-refs' },
195
+ });
196
+ }
197
+
198
+ // Large-working-tree levers share the same evidence; they differ only in tier and eligibility.
199
+ const trackedFilesEvidence = {
200
+ reason: 'trackedFiles',
201
+ value: estimatedTrackedFiles,
202
+ threshold: trackedFilesThreshold,
203
+ } as const;
204
+ const workingTreeLevers: { id: GitOptimizationId; tier: GitOptimizationTier; eligible: boolean }[] = [
205
+ {
206
+ id: 'untrackedCache',
207
+ tier: 'ask',
208
+ // Skip if already on, if it previously failed git's filesystem probe for this repo, or if the
209
+ // user set the key themselves — an explicit `false`/`keep` is a decision (often made after bad
210
+ // `git status` results), and this lever respects it rather than overriding it.
211
+ eligible:
212
+ !snapshot.config.untrackedCache &&
213
+ !snapshot.config.untrackedCacheConfigured &&
214
+ !snapshot.untrackedCacheNotApplicable,
215
+ },
216
+ {
217
+ id: 'fsmonitor',
218
+ tier: 'ask',
219
+ // Skip if already on or it previously failed to start for this repo.
220
+ eligible: !snapshot.config.fsmonitor && !snapshot.fsmonitorNotApplicable,
221
+ },
222
+ {
223
+ id: 'manyFiles',
224
+ tier: 'ask',
225
+ // `manyFiles` defaults the untracked cache on, so a repo whose filesystem already failed that
226
+ // probe must not be offered it either — unless the user's own explicit `core.untrackedCache`
227
+ // setting overrides the default, in which case the probe result is moot.
228
+ eligible:
229
+ !snapshot.config.manyFiles &&
230
+ !(snapshot.untrackedCacheNotApplicable && !snapshot.config.untrackedCacheConfigured),
231
+ },
232
+ {
233
+ id: 'sparseIndex',
234
+ tier: 'ask',
235
+ // Sparse-directory entries are safe only in cone mode. Require a normal full index so this
236
+ // recommendation is based on the repository-wide path count, not a split/conflicted approximation.
237
+ eligible:
238
+ snapshot.repository.sparseCheckout === true &&
239
+ snapshot.repository.sparseCheckoutCone === true &&
240
+ snapshot.repository.sparseIndex === false &&
241
+ snapshot.repository.splitIndex === false &&
242
+ snapshot.indexEntryCountType === 'full',
243
+ },
244
+ ];
245
+ const worktreeSlowness = slowness?.worktree;
246
+ const worktreeSlownessObserved = (worktreeSlowness?.count ?? 0) > 0;
247
+ if (largeWorkingTree || worktreeSlownessObserved) {
248
+ const evidence: Pick<GitHealthFinding, 'reason' | 'value' | 'threshold'> = largeWorkingTree
249
+ ? trackedFilesEvidence
250
+ : {
251
+ reason: 'worktreeSlowness',
252
+ value: worktreeSlowness?.maxDurationMs ?? 0,
253
+ threshold: slowCommandThresholdMs,
254
+ };
255
+ for (const lever of workingTreeLevers) {
256
+ if (!lever.eligible || !supported(lever.id)) continue;
257
+
258
+ findings.push({
259
+ tier: lever.tier,
260
+ ...evidence,
261
+ action: { kind: 'optimization', id: lever.id },
262
+ });
263
+ }
264
+ }
265
+
266
+ // Ask tier — system-scheduled background maintenance. Suggested for a not-yet-registered repo that's
267
+ // clearly large. Passive slowness is deliberately NOT enough: worktree slowness maps to the worktree
268
+ // levers above, history is already covered by demand commit-graph writes, and ref/object maintenance has
269
+ // its own measured auto-tier conditions. A generic scheduler recommendation would promise the wrong fix.
270
+ // `=== false`, not falsy: `undefined` means the registration list couldn't be read, and an unknown state
271
+ // must never be suggested away — that's how a user's own registration gets silently reclaimed by Undo.
272
+ if (clearlyLarge && snapshot.maintenanceRegistered === false && supported('backgroundMaintenance')) {
273
+ // Resolve the evidence once so reason/value/threshold can never drift apart. Ordered so each
274
+ // suggestion argues from its most DISTINCTIVE signal rather than whichever crossed first: a repo
275
+ // large by both file count and pack bytes would otherwise attach the same tracked-files evidence to
276
+ // this finding as to the fsmonitor/manyFiles findings, and the view would show two identical meters.
277
+ let evidence: Pick<GitHealthFinding, 'reason' | 'value' | 'threshold'>;
278
+ if (snapshot.packBytes >= largePackBytesThreshold) {
279
+ evidence = { reason: 'largePacks', value: snapshot.packBytes, threshold: largePackBytesThreshold };
280
+ } else {
281
+ evidence = trackedFilesEvidence;
282
+ }
283
+ findings.push({
284
+ tier: 'ask',
285
+ ...evidence,
286
+ action: { kind: 'optimization', id: 'backgroundMaintenance' },
287
+ });
288
+ }
289
+
290
+ return {
291
+ findings: findings,
292
+ repository: snapshot.repository,
293
+ clearlyLarge: clearlyLarge,
294
+ estimatedLooseObjects: estimatedLooseObjects,
295
+ looseRefs: snapshot.looseRefs,
296
+ estimatedTrackedFiles: estimatedTrackedFiles,
297
+ trackedFilesExact: trackedFilesExact,
298
+ trackedFilesScope: trackedFilesScope,
299
+ packCount: snapshot.packCount,
300
+ packsOutsideMultiPackIndex: snapshot.packsOutsideMultiPackIndex,
301
+ packBytes: snapshot.packBytes,
302
+ commitGraph: snapshot.commitGraph,
303
+ };
304
+ }
305
+
306
+ /**
307
+ * What the view shows for one lever. Ownership is the load-bearing distinction: `applied` offers Undo
308
+ * because GitLens recorded the exact prior; `userEnabled` offers nothing at all — there is nothing for
309
+ * GitLens to undo, and a disabled control would imply otherwise.
310
+ */
311
+ export type GitHealthLeverStatus =
312
+ | 'suggested'
313
+ | 'applied'
314
+ | 'userEnabled'
315
+ /** Supported and usable here, just not worth recommending for this repo — NOT a failure. */
316
+ | 'available'
317
+ | 'unavailable'
318
+ | 'notApplicable';
319
+
320
+ export interface GitHealthLever {
321
+ readonly id: GitOptimizationId;
322
+ readonly status: GitHealthLeverStatus;
323
+ readonly tier: GitOptimizationTier;
324
+ /** Why it can't be used here — rendered verbatim, never flattened to "unsupported". */
325
+ readonly reason?: string;
326
+ /** A consequence the person must see BEFORE choosing, not after. */
327
+ readonly note?: string;
328
+ /**
329
+ * `true` when this lever's state could not be DETERMINED (e.g. the registration list was unreadable), as
330
+ * opposed to a lever this git/platform doesn't support.
331
+ */
332
+ readonly checkFailed?: boolean;
333
+ }
334
+
335
+ /** Consequences that reach beyond GitLens, surfaced on the apply action itself. */
336
+ const leverNotes: Partial<Record<GitOptimizationId, string>> = {
337
+ backgroundMaintenance:
338
+ 'Adds a task to your operating system’s scheduler. Undo unregisters this repository, but leaves the scheduler itself in place.',
339
+ };
340
+
341
+ /**
342
+ * Collapses a snapshot + capabilities + report into the per-lever rows the Git Health view renders.
343
+ * Pure, so the webview never re-derives ownership or eligibility rules that already live here.
344
+ */
345
+ export function computeLevers(
346
+ snapshot: GitHealthSnapshot,
347
+ capabilities: readonly GitOptimizationCapability[],
348
+ report: GitHealthReport,
349
+ ): GitHealthLever[] {
350
+ const suggested = new Set<GitOptimizationId>(
351
+ report.findings.flatMap(f => (f.action.kind === 'optimization' ? [f.action.id] : [])),
352
+ );
353
+
354
+ const enabled: Record<GitOptimizationId, boolean> = {
355
+ untrackedCache: snapshot.config.untrackedCache,
356
+ fsmonitor: snapshot.config.fsmonitor,
357
+ manyFiles: snapshot.config.manyFiles,
358
+ // `=== true`, not the raw (possibly `undefined`) value — an unreadable registration is handled as its
359
+ // own status below, and must never fall through to "enabled".
360
+ backgroundMaintenance: snapshot.maintenanceRegistered === true,
361
+ sparseIndex: snapshot.repository.sparseIndex === true,
362
+ };
363
+ const blocked: Partial<Record<GitOptimizationId, boolean>> = {
364
+ untrackedCache: snapshot.untrackedCacheNotApplicable,
365
+ fsmonitor: snapshot.fsmonitorNotApplicable,
366
+ manyFiles: snapshot.untrackedCacheNotApplicable && !snapshot.config.untrackedCacheConfigured,
367
+ };
368
+
369
+ return capabilities.map<GitHealthLever>(capability => {
370
+ const id = capability.id;
371
+ const note = capability.note ?? leverNotes[id];
372
+ // Every optimization lever is ask-tier now — the auto tier only ever runs maintenance tasks.
373
+ const tier: GitOptimizationTier = 'ask';
374
+
375
+ // Unavailable outranks everything: a lever the installed git or platform cannot do is never
376
+ // "suggested", whatever the thresholds say.
377
+ if (!capability.supported) {
378
+ return { id: id, status: 'unavailable', tier: tier, reason: capability.reason, note: note };
379
+ }
380
+ // An unreadable maintenance registration can't answer the ownership/suggestion questions below, so
381
+ // it gets its own status rather than falling through and being guessed as "not registered".
382
+ if (id === 'backgroundMaintenance' && snapshot.maintenanceRegistered === undefined) {
383
+ return {
384
+ id: id,
385
+ status: 'unavailable',
386
+ tier: tier,
387
+ reason: "Couldn't read Git's maintenance registration — try reopening the repository",
388
+ note: note,
389
+ checkFailed: true,
390
+ };
391
+ }
392
+ // Then ownership, which is what decides whether Undo is offered at all.
393
+ if (enabled[id]) {
394
+ return { id: id, status: snapshot.applied[id] ? 'applied' : 'userEnabled', tier: tier, note: note };
395
+ }
396
+ if (id === 'sparseIndex') {
397
+ if (
398
+ snapshot.repository.sparseCheckout == null ||
399
+ snapshot.repository.sparseCheckoutCone == null ||
400
+ snapshot.repository.sparseIndex == null ||
401
+ snapshot.repository.splitIndex == null
402
+ ) {
403
+ return {
404
+ id: id,
405
+ status: 'unavailable',
406
+ tier: tier,
407
+ reason: "Couldn't determine this worktree’s sparse-checkout and index configuration",
408
+ note: note,
409
+ checkFailed: true,
410
+ };
411
+ }
412
+ if (!snapshot.repository.sparseCheckout) {
413
+ return {
414
+ id: id,
415
+ status: 'notApplicable',
416
+ tier: tier,
417
+ reason: 'This worktree is not using sparse checkout.',
418
+ note: note,
419
+ };
420
+ }
421
+ if (!snapshot.repository.sparseCheckoutCone) {
422
+ return {
423
+ id: id,
424
+ status: 'notApplicable',
425
+ tier: tier,
426
+ reason: 'Sparse indexes require cone-mode sparse checkout.',
427
+ note: note,
428
+ };
429
+ }
430
+ if (snapshot.repository.splitIndex || snapshot.indexEntryCountType === 'split') {
431
+ return {
432
+ id: id,
433
+ status: 'notApplicable',
434
+ tier: tier,
435
+ reason: 'This worktree uses a split index; disable it before enabling a sparse index.',
436
+ note: note,
437
+ };
438
+ }
439
+ if (snapshot.indexEntryCountType === 'conflicted') {
440
+ return {
441
+ id: id,
442
+ status: 'notApplicable',
443
+ tier: tier,
444
+ reason: 'Finish the current merge or rebase before enabling a sparse index.',
445
+ note: note,
446
+ };
447
+ }
448
+ }
449
+ // `notApplicable` means GitLens TRIED here and this repo can't use it — the gk marker records that.
450
+ // `capability.note` is the supported-with-caveat field, not an unavailability reason, so the
451
+ // explanation has to come from the marker's meaning instead.
452
+ if (blocked[id]) {
453
+ return {
454
+ id: id,
455
+ status: 'notApplicable',
456
+ tier: tier,
457
+ reason:
458
+ id === 'fsmonitor'
459
+ ? 'The file system monitor could not start for this repository.'
460
+ : id === 'manyFiles'
461
+ ? 'This would enable the untracked cache, but this file system does not report directory changes reliably, so Git would return incorrect status results.'
462
+ : 'This file system does not report directory changes reliably, so Git would return incorrect status results.',
463
+ note: note,
464
+ };
465
+ }
466
+
467
+ // Supported and usable, just below the thresholds — `available`, never `notApplicable`. Reporting
468
+ // a healthy repo's levers as unavailable tells the user something false.
469
+ return { id: id, status: suggested.has(id) ? 'suggested' : 'available', tier: tier, note: note };
470
+ });
471
+ }
472
+
473
+ /** Auto-tier maintenance tasks the daily pass should run right now (deduped, order-stable). */
474
+ export function getAutoMaintenanceTasks(report: GitHealthReport): GitMaintenanceTask[] {
475
+ const tasks: GitMaintenanceTask[] = [];
476
+ for (const finding of report.findings) {
477
+ if (finding.tier === 'auto' && finding.action.kind === 'maintenance' && !tasks.includes(finding.action.task)) {
478
+ tasks.push(finding.action.task);
479
+ }
480
+ }
481
+ return tasks;
482
+ }
483
+
484
+ /** Auto-tier optimization levers the daily pass should silently apply — none today; the auto tier currently
485
+ * applies only maintenance tasks, but this stays in place for future levers. */
486
+ export function getAutoOptimizations(report: GitHealthReport): GitOptimizationId[] {
487
+ const ids: GitOptimizationId[] = [];
488
+ for (const finding of report.findings) {
489
+ if (finding.tier === 'auto' && finding.action.kind === 'optimization' && !ids.includes(finding.action.id)) {
490
+ ids.push(finding.action.id);
491
+ }
492
+ }
493
+ return ids;
494
+ }
495
+
496
+ /**
497
+ * Banner rule: fire only when the report contains an ask-tier fix and the repo is clearly large or slow in
498
+ * the working-tree family. History/ref/object slowness must not advertise unrelated worktree levers.
499
+ */
500
+ export function isBannerEligible(report: GitHealthReport, slowness: GitHealthSlowness | undefined): boolean {
501
+ const hasAskTierFix = report.findings.some(f => f.tier === 'ask');
502
+ const worktreeSlownessObserved = (slowness?.worktree?.count ?? 0) > 0;
503
+ return hasAskTierFix && (report.clearlyLarge || worktreeSlownessObserved);
504
+ }
505
+
506
+ /** Evidence-gated banner state for one repository — undefined when the banner has nothing to say. */
507
+ export interface GitHealthBannerState {
508
+ /** Show the in-graph banner strip (eligible and not explicitly dismissed). */
509
+ readonly banner: boolean;
510
+ /** Show the evidence dot on the Visualizations & Health toggle (eligible, view not visited recently). */
511
+ readonly indicator: boolean;
512
+ /** Which evidence family armed it — measured worktree slowness, or repo scale alone. */
513
+ readonly reason: 'slowness' | 'large';
514
+ /** Worst observed slow-command duration (ms); present when reason is 'slowness'. */
515
+ readonly maxDurationMs?: number;
516
+ /** Tracked-file count evidence; present when reason is 'large'. */
517
+ readonly trackedFiles?: number;
518
+ /** Whether trackedFiles is exact (report.trackedFilesScope !== 'estimate'). */
519
+ readonly trackedFilesExact?: boolean;
520
+ /** Count of levers the health view will show as "suggested" — keep in sync with its verdict. */
521
+ readonly suggestedCount: number;
522
+ }
@@ -1,7 +1,9 @@
1
1
  type SearchOperatorsShortForm = '' | '=:' | '@:' | '#:' | '?:' | '~:' | 'is:' | '>:' | '<:' | '^:';
2
2
  export type SearchOperatorsLongForm =
3
3
  | 'message:'
4
+ | '-message:'
4
5
  | 'author:'
6
+ | 'committer:'
5
7
  | 'commit:'
6
8
  | 'file:'
7
9
  | 'change:'
@@ -17,8 +19,10 @@ export const searchOperators = new Set<string>([
17
19
  '',
18
20
  '=:',
19
21
  'message:',
22
+ '-message:',
20
23
  '@:',
21
24
  'author:',
25
+ 'committer:',
22
26
  '#:',
23
27
  'commit:',
24
28
  '?:',
@@ -41,8 +45,10 @@ export const searchOperatorsToLongFormMap = new Map<SearchOperators, SearchOpera
41
45
  ['', 'message:'],
42
46
  ['=:', 'message:'],
43
47
  ['message:', 'message:'],
48
+ ['-message:', '-message:'],
44
49
  ['@:', 'author:'],
45
50
  ['author:', 'author:'],
51
+ ['committer:', 'committer:'],
46
52
  ['#:', 'commit:'],
47
53
  ['commit:', 'commit:'],
48
54
  ['?:', 'file:'],
@@ -62,11 +68,20 @@ export const searchOperatorsToLongFormMap = new Map<SearchOperators, SearchOpera
62
68
  ]);
63
69
 
64
70
  export const searchOperationHelpRegex =
65
- /(?:^|(\b|\s)*)((=:|message:|@:|author:|#:|commit:|\?:|file:|~:|change:|is:|type:|>:|after:|since:|<:|before:|until:|\^:|ref:)(?:"[^"]*"?|[^\s]*))(?:$|(\b|\s))/g;
71
+ /(?:^|(\b|\s)*)((=:|message:|-message:|@:|author:|committer:|#:|commit:|\?:|file:|~:|change:|is:|type:|>:|after:|since:|<:|before:|until:|\^:|ref:)(?:"[^"]*"?|[^\s]*))(?:$|(\b|\s))/g;
66
72
 
67
73
  export interface SearchQuery {
68
74
  query: string;
69
- naturalLanguage?: boolean | { query: string; processedQuery?: string; error?: string };
75
+ naturalLanguage?:
76
+ | boolean
77
+ | {
78
+ query: string;
79
+ processedQuery?: string;
80
+ error?: string;
81
+ explanation?: string;
82
+ mode?: 'highlight' | 'filter' | 'select';
83
+ alternates?: string[];
84
+ };
70
85
 
71
86
  filter?: boolean;
72
87
  matchAll?: boolean;
@@ -96,8 +111,8 @@ export interface ParsedSearchQuery {
96
111
  export interface SearchQueryFilters {
97
112
  /** Specifies whether the search results will be filtered to specific files */
98
113
  files: boolean;
99
- /** Specifies whether the search results will be filtered to a specific type, only `stash`, `tip`, and `wip` are supported */
100
- type?: 'stash' | 'tip' | 'wip';
114
+ /** Specifies whether the search results will be filtered to a specific type, only `stash`, `tip`, `wip`, and `merge` are supported */
115
+ type?: 'stash' | 'tip' | 'wip' | 'merge';
101
116
  /** Specifies whether the search results will be filtered to a specific ref or ref range */
102
117
  refs: boolean;
103
118
  }
@@ -219,18 +219,41 @@ export async function filterDiffFiles(
219
219
  .join('');
220
220
  }
221
221
 
222
- /** Counts insertions and deletions from a parsed diff file */
222
+ /**
223
+ * Counts the added and removed lines of a parsed diff file, matching what `git diff --numstat`
224
+ * reports for it. Binary files and files without content changes (pure renames, mode changes)
225
+ * count as 0/0.
226
+ */
223
227
  export function countDiffInsertionsAndDeletions(file: ParsedGitDiffFile): { insertions: number; deletions: number } {
224
228
  let insertions = 0;
225
229
  let deletions = 0;
226
230
  for (const hunk of file.hunks) {
227
- insertions += hunk.current.count;
228
- deletions += hunk.previous.count;
231
+ // Counted off the hunk's raw content rather than its header counts (which include unchanged
232
+ // context) or `hunk.lines` (which merges a removed/added pair into one `changed` entry and
233
+ // lets a following context line overwrite the tail of an asymmetric block). Scanned in place
234
+ // rather than split into lines: splitting a multi-megabyte diff allocates a string per line
235
+ // for a single pass that only ever looks at each line's first character.
236
+ const { content } = hunk;
237
+ for (let start = 0; start < content.length;) {
238
+ const marker = content[start];
239
+ if (marker === '+') {
240
+ insertions++;
241
+ } else if (marker === '-') {
242
+ deletions++;
243
+ }
244
+
245
+ const end = content.indexOf('\n', start);
246
+ if (end === -1) break;
247
+
248
+ start = end + 1;
249
+ }
229
250
  }
230
251
  return { insertions: insertions, deletions: deletions };
231
252
  }
232
253
 
233
- /** Counts approximate number of changed lines in a diff file */
254
+ /** Counts the approximate number of changed lines in a diff file — the size of both sides of its
255
+ * hunks, unchanged context included. Suitable for relative sizing, not for reporting a line count
256
+ * to the user; use {@link countDiffInsertionsAndDeletions} for that. */
234
257
  export function countDiffLines(file: ParsedGitDiffFile): number {
235
258
  let count = 0;
236
259
  for (const hunk of file.hunks) {