@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
@@ -23,6 +23,7 @@ import { ConfigGitSubProvider } from './providers/config.js';
23
23
  import { ContributorsGitSubProvider } from './providers/contributors.js';
24
24
  import { DiffGitSubProvider } from './providers/diff.js';
25
25
  import { GraphGitSubProvider } from './providers/graph.js';
26
+ import { MaintenanceGitSubProvider } from './providers/maintenance.js';
26
27
  import { OperationsGitSubProvider } from './providers/operations.js';
27
28
  import { PatchGitSubProvider } from './providers/patch.js';
28
29
  import { PausedOperationsGitSubProvider } from './providers/pausedOperations.js';
@@ -46,6 +47,13 @@ export interface CliGitProviderOptions {
46
47
  git?: Git;
47
48
  /** Git execution options (timeout, trust, queue config, hooks, etc.) */
48
49
  gitOptions?: GitOptions;
50
+ /**
51
+ * Whether repositories reached through this provider are on the LOCAL filesystem. `false` disables the
52
+ * maintenance sub-provider entirely (Live Share): its probes stat local paths, and its writes are
53
+ * rejected by the host's command allowlist, so inheriting it yields empty reports and swallowed
54
+ * failures rather than an honest "unsupported here". Defaults to `true`.
55
+ */
56
+ localRepositories?: boolean;
49
57
  }
50
58
 
51
59
  /**
@@ -83,9 +91,11 @@ export class CliGitProvider implements GitProvider {
83
91
  private readonly _git: Git;
84
92
 
85
93
  readonly context: GitServiceContext;
94
+ private readonly _localRepositories: boolean;
86
95
 
87
96
  constructor(options: CliGitProviderOptions) {
88
97
  this.context = options.context;
98
+ this._localRepositories = options.localRepositories ?? true;
89
99
  this._git =
90
100
  options.git ??
91
101
  new Git(options.locator, {
@@ -220,6 +230,21 @@ export class CliGitProvider implements GitProvider {
220
230
  ));
221
231
  }
222
232
 
233
+ private _maintenance: MaintenanceGitSubProvider | undefined;
234
+ // Optional so subclasses whose repos aren't local can report the capability as absent — see
235
+ // `VslsGitProvider`. Consumers already treat `maintenance == null` as "unsupported here" (web builds
236
+ // never register it at all).
237
+ get maintenance(): MaintenanceGitSubProvider | undefined {
238
+ if (!this._localRepositories) return undefined;
239
+
240
+ return (this._maintenance ??= new MaintenanceGitSubProvider(
241
+ this.context,
242
+ this._git,
243
+ this._cache,
244
+ this as unknown as CliGitProviderInternal,
245
+ ));
246
+ }
247
+
223
248
  private _ops: OperationsGitSubProvider | undefined;
224
249
  get ops(): OperationsGitSubProvider {
225
250
  return (this._ops ??= new OperationsGitSubProvider(
@@ -8,6 +8,7 @@ import type {
8
8
  CommitErrorReason,
9
9
  FetchErrorReason,
10
10
  GitCommandError,
11
+ GitSearchErrorReason,
11
12
  GitWarningKey,
12
13
  MergeErrorReason,
13
14
  PausedOperationAbortErrorReason,
@@ -45,7 +46,7 @@ import type { GitCommandPriority, GitResult, GitRunOptions, GitSpawnOptions } fr
45
46
  import type { FilteredGitFeatures, GitFeatureOrPrefix, GitFeatures } from './features.js';
46
47
  import { gitFeaturesByVersion } from './features.js';
47
48
  import type { GitQueueConfig } from './gitQueue.js';
48
- import { GitQueue, inferGitCommandPriority } from './gitQueue.js';
49
+ import { getPrimaryGitCommand, GitQueue, inferGitCommandPriority } from './gitQueue.js';
49
50
  import type { GitLocation } from './locator.js';
50
51
 
51
52
  const slowCallWarningThreshold = 2000;
@@ -62,7 +63,7 @@ export const gitConfigsStatus = ['-c', 'color.status=false'] as const;
62
63
  export const GitErrors = {
63
64
  alreadyCheckedOut: /already checked out/i,
64
65
  alreadyExists: /already exists/i,
65
- ambiguousArgument: /fatal:\s*ambiguous argument ['"].+['"]: unknown revision or path not in the working tree/i,
66
+ ambiguousArgument: /fatal:\s*ambiguous argument ['"](.+?)['"]: unknown revision or path not in the working tree/i,
66
67
  badObject: /fatal:\s*bad object (.*?)/i,
67
68
  badRevision: /bad revision '(.*?)'/i,
68
69
  branchAlreadyExists: /fatal:\s*A branch named '.+?' already exists/i,
@@ -83,6 +84,10 @@ export const GitErrors = {
83
84
  invalidLineCount: /file .+? has only (\d+) lines/i,
84
85
  invalidObjectName: /invalid object name: (.*)\s/i,
85
86
  invalidObjectNameList: /could not open object name list: (.*)\s/i,
87
+ // Regcomp failure reasons from `-G`/pathspec regex compilation (search); matched without a known
88
+ // prefix as a fallback when the reason appears in an unrecognized stderr shape
89
+ invalidSearchPattern:
90
+ /Unmatched \( or \\\(|Unmatched \[|Unmatched \) or \\\)|Invalid content of \\\{\\\}|Invalid preceding regular expression|Invalid regular expression|Trailing backslash|invalid regex/i,
86
91
  invalidTagName: /invalid tag name/i,
87
92
  mainWorkingTree: /is a main working tree/i,
88
93
  mergeAborted: /merge.*aborted/i,
@@ -100,6 +105,7 @@ export const GitErrors = {
100
105
  /no merge (?:in progress|to abort)|no cherry-pick(?: or revert)? in progress|no rebase in progress/i,
101
106
  permissionDenied: /Permission.*denied/i,
102
107
  previousOperationEmpty: /The previous (?:cherry-pick|revert) is now empty/i,
108
+ problemWithEditor: /there was a problem with the editor/i,
103
109
  pushRejected: /^error:\s*failed to push some refs to\b/m,
104
110
  pushRejectedRefDoesNotExists: /error:\s*unable to delete '(.*?)': remote ref does not exist/m,
105
111
  rebaseAborted: /Nothing to do|rebase.*aborted/i,
@@ -277,6 +283,9 @@ const errorToReasonMap = new Map<GitCommand, [RegExp, GitCommandToReasonMap[GitC
277
283
  [
278
284
  'paused-operation-continue',
279
285
  [
286
+ // Kept FIRST: an editor failure's stderr can also carry broader messages ("could not
287
+ // commit staged changes", …) that later patterns would otherwise claim
288
+ [GitErrors.problemWithEditor, 'messageEditFailed'],
280
289
  [GitErrors.previousOperationEmpty, 'emptyCommit'],
281
290
  [GitErrors.noPausedOperation, 'nothingToContinue'],
282
291
  [GitErrors.uncommittedChanges, 'uncommittedChanges'],
@@ -316,8 +325,11 @@ const errorToReasonMap = new Map<GitCommand, [RegExp, GitCommandToReasonMap[GitC
316
325
  [
317
326
  [GitErrors.remoteAhead, 'remoteAhead'],
318
327
  [GitWarnings.tipBehind, 'tipBehind'],
319
- [GitErrors.pushRejected, 'rejected'],
328
+ // Kept BEFORE the generic push rejection: a failed `push -d` of a missing ref reports both
329
+ // "unable to delete '<ref>': remote ref does not exist" and "failed to push some refs to ...",
330
+ // and the specific reason is the actionable one.
320
331
  [GitErrors.pushRejectedRefDoesNotExists, 'rejectedRefDoesNotExist'],
332
+ [GitErrors.pushRejected, 'rejected'],
321
333
  [GitErrors.permissionDenied, 'permissionDenied'],
322
334
  [GitErrors.remoteConnectionFailed, 'remoteConnectionFailed'],
323
335
  [GitErrors.noUpstream, 'noUpstream'],
@@ -383,6 +395,7 @@ const errorToReasonMap = new Map<GitCommand, [RegExp, GitCommandToReasonMap[GitC
383
395
  [GitErrors.tagNotFound, 'notFound'],
384
396
  [GitErrors.permissionDenied, 'permissionDenied'],
385
397
  [GitErrors.remoteRejected, 'remoteRejected'],
398
+ [GitErrors.tagConflict, 'tagConflict'],
386
399
  ],
387
400
  ],
388
401
  [
@@ -469,6 +482,44 @@ export function inferSigningFormatFromError(ex: unknown): SigningFormat | undefi
469
482
  return undefined;
470
483
  }
471
484
 
485
+ export interface SearchErrorClassification {
486
+ reason: GitSearchErrorReason;
487
+ detail?: string;
488
+ }
489
+
490
+ /**
491
+ * Classifies a graph search failure from its stderr into an {@link GitSearchErrorReason}, with the
492
+ * offending pattern/ref text (when git reports one) as `detail`. Returns `undefined` when the error
493
+ * isn't a `GitError` with stderr, or the stderr doesn't match a recognized shape.
494
+ */
495
+ export function classifySearchError(ex: unknown): SearchErrorClassification | undefined {
496
+ if (!(ex instanceof GitError) || !ex.stderr) return undefined;
497
+
498
+ const stderr = ex.stderr;
499
+
500
+ // `-G<pattern>`/pathspec regex compile failures report one of two shapes:
501
+ // fatal: command line, '<pattern>': <regcomp reason>
502
+ // fatal: invalid regex: <regcomp reason>
503
+ // The pattern itself may contain "': ", so anchor on the known prefixes and let the greedy `.*`
504
+ // resolve to the LAST matching delimiter rather than splitting on the first occurrence.
505
+ const patternDetail =
506
+ /^fatal:\s*command line,\s*'.*':\s*(.+)$/im.exec(stderr)?.[1] ??
507
+ /^fatal:\s*invalid regex:\s*(.+)$/im.exec(stderr)?.[1];
508
+ if (patternDetail != null) return { reason: 'invalidPattern', detail: patternDetail.trim() };
509
+
510
+ if (GitErrors.invalidSearchPattern.test(stderr)) return { reason: 'invalidPattern' };
511
+
512
+ const ambiguous = GitErrors.ambiguousArgument.exec(stderr);
513
+ if (ambiguous != null) return { reason: 'invalidRef', detail: ambiguous[1] };
514
+
515
+ const badRevision = GitErrors.badRevision.exec(stderr);
516
+ if (badRevision != null) return { reason: 'invalidRef', detail: badRevision[1] };
517
+
518
+ if (/unknown revision/i.test(stderr)) return { reason: 'invalidRef' };
519
+
520
+ return undefined;
521
+ }
522
+
472
523
  export interface GitOptions {
473
524
  /** Custom environment variables to add to every git command */
474
525
  env?: Record<string, string | undefined>;
@@ -504,6 +555,12 @@ export interface GitHooks {
504
555
  queued: Record<GitCommandPriority, number>;
505
556
  maxConcurrent: number;
506
557
  }): void;
558
+ /**
559
+ * Called once per git command that ran slowly (over the slow-call threshold). Fired only for the
560
+ * command that actually executed, never for a deduplicated rider that merely awaited it, so a single
561
+ * slow subprocess counts once. `operation` is the primary git subcommand (e.g. `status`, `rev-list`).
562
+ */
563
+ onSlowCommand?(info: { operation: string | undefined; cwd: string | undefined; duration: number }): void;
507
564
  }
508
565
 
509
566
  const emptyArray: readonly never[] = Object.freeze([]);
@@ -709,6 +766,7 @@ export class Git {
709
766
  errors: errorHandling,
710
767
  encoding,
711
768
  runLocally: _,
769
+ selfMaintenance,
712
770
  ...opts
713
771
  } = options;
714
772
 
@@ -726,6 +784,9 @@ export class Git {
726
784
  options?.stdin != null ? `${uniqueCounterForStdin.next()}:` : ''
727
785
  }${cancellation != null ? `${getAbortSignalId(cancellation)}:` : ''}${gitCommand}`;
728
786
 
787
+ // When the subprocess actually started — `start` includes GitQueue wait, which is congestion rather
788
+ // than anything the repository did. Stays undefined for a dedup rider or a command aborted while queued.
789
+ let execStart: ReturnType<typeof hrtime> | undefined;
729
790
  let waiting;
730
791
  let promise = this.pendingCommands.get(cacheKey);
731
792
  if (promise == null) {
@@ -758,7 +819,14 @@ export class Git {
758
819
  // Execute through the queue (interactive/normal run immediately, background is throttled)
759
820
  const gitPath = await this.path();
760
821
  void this._queue
761
- .run(priority, () => runSpawn<T>(gitPath, args, encoding ?? 'utf8', runOpts), cancellation)
822
+ .run(
823
+ priority,
824
+ () => {
825
+ execStart = hrtime();
826
+ return runSpawn<T>(gitPath, args, encoding ?? 'utf8', runOpts);
827
+ },
828
+ cancellation,
829
+ )
762
830
  .then(deferred.fulfill, (e: unknown) => deferred.cancel(e instanceof Error ? e : new Error(String(e))))
763
831
  .finally(() => {
764
832
  this.pendingCommands.delete(cacheKey);
@@ -908,7 +976,17 @@ export class Git {
908
976
  completion: { status: 'warned', warning: warning, error: swallowed },
909
977
  };
910
978
  } finally {
911
- this.logGitCommandComplete(gitCommand, exception, getDurationMilliseconds(start), waiting);
979
+ this.logGitCommandComplete(
980
+ gitCommand,
981
+ exception,
982
+ getDurationMilliseconds(start),
983
+ // 0 when the subprocess never started — there is no execution time to attribute. Also 0 for
984
+ // GitLens's own maintenance work, which is slow by design and must not read as repo slowness.
985
+ execStart == null || selfMaintenance ? 0 : getDurationMilliseconds(execStart),
986
+ waiting,
987
+ options.cwd,
988
+ args,
989
+ );
912
990
  }
913
991
  }
914
992
 
@@ -1013,7 +1091,20 @@ export class Git {
1013
1091
  try {
1014
1092
  proc.removeAllListeners();
1015
1093
  } catch {}
1016
- this.logGitCommandComplete(gitCommand, exception, getDurationMilliseconds(start), false, streamId);
1094
+ // Streaming spawns directly, with no GitQueue wait to exclude. It isn't pure subprocess time
1095
+ // either — the generator applies backpressure, so a slow consumer stalls the pipe and inflates
1096
+ // this — but there's no separate signal to measure here.
1097
+ const duration = getDurationMilliseconds(start);
1098
+ this.logGitCommandComplete(
1099
+ gitCommand,
1100
+ exception,
1101
+ duration,
1102
+ duration,
1103
+ false,
1104
+ spawnOpts.cwd as string | undefined,
1105
+ runArgs,
1106
+ streamId,
1107
+ );
1017
1108
  };
1018
1109
 
1019
1110
  try {
@@ -1175,12 +1266,35 @@ export class Git {
1175
1266
  command: string,
1176
1267
  ex: Error | undefined,
1177
1268
  duration: number,
1269
+ /** Subprocess-only duration (excludes GitQueue wait); 0 when it never ran. */
1270
+ execDuration: number,
1178
1271
  waiting: boolean,
1272
+ cwd: string | undefined,
1273
+ args: readonly (string | undefined)[] | undefined,
1179
1274
  id?: number,
1180
1275
  ): void {
1181
1276
  const slow = duration > slowCallWarningThreshold;
1182
1277
  const status = slow && waiting ? ' (slow, waiting)' : waiting ? ' (waiting)' : slow ? ' (slow)' : '';
1183
1278
 
1279
+ // The health signal is gated on the SUBPROCESS time, not the total: time spent queued behind other
1280
+ // git work is congestion, not repository slowness, and counting it would let a busy queue (including
1281
+ // our own background maintenance holding slots) manufacture the evidence that recommends more
1282
+ // maintenance. Logging keeps the total — the wait is exactly what you want to see when diagnosing.
1283
+ //
1284
+ // A deduplicated rider (`waiting`) shares the executed command's duration, so counting it too
1285
+ // would double-count a single slow subprocess — fire only for the command that actually ran.
1286
+ // Guarded: this runs inside the command's `finally`, so a hook throw would otherwise replace
1287
+ // the command's real result/error for the caller.
1288
+ if (execDuration > slowCallWarningThreshold && !waiting) {
1289
+ try {
1290
+ this.options.hooks?.onSlowCommand?.({
1291
+ operation: args != null ? getPrimaryGitCommand(args) : undefined,
1292
+ cwd: cwd,
1293
+ duration: execDuration,
1294
+ });
1295
+ } catch {}
1296
+ }
1297
+
1184
1298
  if (ex != null) {
1185
1299
  Logger.error(
1186
1300
  undefined,
@@ -278,20 +278,8 @@ const optionsTakingValue = new Set([
278
278
  '--list-cmds',
279
279
  ]);
280
280
 
281
- /**
282
- * Infers a queue priority from the git command being invoked.
283
- *
284
- * Intentionally narrow: only returns 'background' for commands that are
285
- * *always* read-only AND *always* potentially expensive. Polymorphic commands
286
- * like `log` and `rev-list` (which can be quick bounded lookups or full-history
287
- * walks) are deliberately omitted — call sites that perform heavy walks must
288
- * pass `priority: 'background'` explicitly. Never upgrades to 'interactive'.
289
- *
290
- * No subcommand awareness (e.g. `stash list`, `branch -l`): the risk of
291
- * misclassifying a write subcommand as background outweighs the gain.
292
- */
293
- export function inferGitCommandPriority(args: readonly (string | undefined)[]): GitCommandPriority {
294
- let command: string | undefined;
281
+ /** Extracts the primary git subcommand from an arg list, skipping global options and their values. */
282
+ export function getPrimaryGitCommand(args: readonly (string | undefined)[]): string | undefined {
295
283
  for (let i = 0; i < args.length; i++) {
296
284
  const a = args[i];
297
285
  if (a == null) continue;
@@ -304,11 +292,25 @@ export function inferGitCommandPriority(args: readonly (string | undefined)[]):
304
292
  }
305
293
  if (a.startsWith('-')) continue;
306
294
 
307
- command = a;
308
- break;
295
+ return a;
309
296
  }
297
+ return undefined;
298
+ }
310
299
 
311
- switch (command) {
300
+ /**
301
+ * Infers a queue priority from the git command being invoked.
302
+ *
303
+ * Intentionally narrow: only returns 'background' for commands that are
304
+ * *always* read-only AND *always* potentially expensive. Polymorphic commands
305
+ * like `log` and `rev-list` (which can be quick bounded lookups or full-history
306
+ * walks) are deliberately omitted — call sites that perform heavy walks must
307
+ * pass `priority: 'background'` explicitly. Never upgrades to 'interactive'.
308
+ *
309
+ * No subcommand awareness (e.g. `stash list`, `branch -l`): the risk of
310
+ * misclassifying a write subcommand as background outweighs the gain.
311
+ */
312
+ export function inferGitCommandPriority(args: readonly (string | undefined)[]): GitCommandPriority {
313
+ switch (getPrimaryGitCommand(args)) {
312
314
  case 'for-each-ref':
313
315
  case 'shortlog':
314
316
  case 'reflog':
@@ -2,7 +2,7 @@ import type { GitFileIndexStatus } from '../../git/models/fileStatus.js';
2
2
  import { diffHunkRegex, diffRegex } from '../../git/parsers/diffParser.js';
3
3
  import { joinPaths, normalizePath } from '../../utils/path.js';
4
4
  import { maybeStopWatch } from '../../utils/stopwatch.js';
5
- import { iterateAsyncByDelimiter, iterateByDelimiter } from '../../utils/string.js';
5
+ import { flattenString, iterateAsyncByDelimiter, iterateByDelimiter } from '../../utils/string.js';
6
6
  import type { ExtractAll } from '../../utils/types.js';
7
7
 
8
8
  const commitsMapping = {
@@ -389,6 +389,9 @@ export function createLogParser<T extends Record<string, string>>(mapping: Extra
389
389
  // semantics exactly: fields beyond the mapping are ignored, and every mapped field is
390
390
  // terminated by a field separator (the format string appends one after each field).
391
391
  function parseRecord(record: string): LogParsedEntry<T> {
392
+ // Fields sliced out of the record become long-lived (graph rows); flatten the record so they
393
+ // pin a record-sized copy rather than the full walk output the record was sliced from.
394
+ record = flattenString(record);
392
395
  const entry = {} as unknown as LogParsedEntry<T>;
393
396
  let start = 0;
394
397
  for (const key of keys) {
@@ -1,5 +1,6 @@
1
1
  import { stat } from 'fs/promises';
2
2
  import type { Cache } from '../../git/cache.js';
3
+ import { shouldEvictBlameCacheEntry } from '../../git/cache.js';
3
4
  import { BlameIgnoreRevsFileBadRevisionError, BlameIgnoreRevsFileError } from '../../git/errors.js';
4
5
  import type {
5
6
  GitBlame,
@@ -75,8 +76,9 @@ export class BlameGitSubProvider implements GitBlameSubProvider {
75
76
  return this.cache.blame.getOrCreate(
76
77
  repoPath,
77
78
  cacheKey,
78
- () => this.getProgressiveBlameCore(repoPath, path, cacheKey, rev, contents, options),
79
+ () => this.getProgressiveBlameCore(repoPath, path, rev, contents, options),
79
80
  {
81
+ evictWhen: shouldEvictBlameCacheEntry,
80
82
  onError: ex => {
81
83
  if (ex instanceof BlameIgnoreRevsFileError || ex instanceof BlameIgnoreRevsFileBadRevisionError) {
82
84
  // Non-retriable — cache as undefined for 5 minutes
@@ -92,7 +94,6 @@ export class BlameGitSubProvider implements GitBlameSubProvider {
92
94
  private async getProgressiveBlameCore(
93
95
  repoPath: string,
94
96
  path: string,
95
- cacheKey: string,
96
97
  rev?: string,
97
98
  contents?: string,
98
99
  options?: GitBlameOptions,
@@ -112,12 +113,6 @@ export class BlameGitSubProvider implements GitBlameSubProvider {
112
113
  // Start streaming in the background
113
114
  void this.streamBlame(writer, repoPath, path, blameOptions, user, mtime);
114
115
 
115
- // If streaming fails, progressive.completed rejects. Evict the cache entry
116
- // so the next call retries instead of serving the failed GitBlameProgressive.
117
- void progressive.completed.catch(() => {
118
- this.cache.blame.delete(repoPath, cacheKey);
119
- });
120
-
121
116
  return progressive;
122
117
  }
123
118
 
@@ -938,13 +938,21 @@ export class BranchesGitSubProvider implements GitBranchesSubProvider {
938
938
  });
939
939
  }
940
940
 
941
- throw new BranchError(
942
- {
943
- action: options?.force ? 'force delete' : 'delete',
944
- branch: branches.join(', '),
945
- gitCommand: { repoPath: repoPath, args: args },
946
- },
941
+ // Classify the failure (e.g. not fully merged) so consumers like the delete wizard can offer a
942
+ // force retry — without this the reason would be undefined and every failure reads as 'other'.
943
+ throw getGitCommandError(
944
+ 'branch',
947
945
  ex,
946
+ reason =>
947
+ new BranchError(
948
+ {
949
+ reason: reason ?? 'other',
950
+ action: options?.force ? 'force delete' : 'delete',
951
+ branch: branches.join(', '),
952
+ gitCommand: { repoPath: repoPath, args: args },
953
+ },
954
+ ex,
955
+ ),
948
956
  );
949
957
  }
950
958
  }
@@ -1022,9 +1030,21 @@ export class BranchesGitSubProvider implements GitBranchesSubProvider {
1022
1030
  });
1023
1031
  }
1024
1032
 
1025
- throw new BranchError(
1026
- { action: 'delete', branch: branches.join(', '), gitCommand: { repoPath: repoPath, args: args } },
1033
+ // Classify against the underlying `git push -d` failure modes; map the "remote ref does not
1034
+ // exist" case onto BranchError's noRemoteReference so consumers get an actionable reason.
1035
+ throw getGitCommandError(
1036
+ 'push',
1027
1037
  ex,
1038
+ reason =>
1039
+ new BranchError(
1040
+ {
1041
+ reason: reason === 'rejectedRefDoesNotExist' ? 'noRemoteReference' : 'other',
1042
+ action: 'delete',
1043
+ branch: branches.join(', '),
1044
+ gitCommand: { repoPath: repoPath, args: args },
1045
+ },
1046
+ ex,
1047
+ ),
1028
1048
  );
1029
1049
  }
1030
1050
  }
@@ -1383,6 +1383,12 @@ export class CommitsGitSubProvider implements GitCommitsSubProvider {
1383
1383
  search = (await this.context.searchQuery?.preprocessQuery?.(search, options?.source)) ?? search;
1384
1384
  }
1385
1385
 
1386
+ // The conversion failed — `search.query` is still the raw English sentence, not a query; running
1387
+ // it as an ERE would produce a regex error about text the user never wrote.
1388
+ if (typeof search.naturalLanguage === 'object' && search.naturalLanguage.error) {
1389
+ return { search: search, log: undefined };
1390
+ }
1391
+
1386
1392
  try {
1387
1393
  const cfg = this.context.config;
1388
1394
  const currentUser = await this.provider.config.getCurrentUser(repoPath);
@@ -1399,6 +1405,11 @@ export class CommitsGitSubProvider implements GitCommitsSubProvider {
1399
1405
  ];
1400
1406
 
1401
1407
  const { args: searchArgs, files, shas, filters } = parseSearchQueryGitCommand(search, currentUser);
1408
+ // Stash commits have 2-3 parents, so `--merges` matches them; excluded in parseCommits.
1409
+ // Derived from the args git actually runs rather than `filters.type`, since a multi-value
1410
+ // `type:` query (e.g. `type:merge type:tip`) leaves `--merges` in the args while
1411
+ // `filters.type` reflects only the last value parsed
1412
+ const mergesOnly = searchArgs.includes('--merges');
1402
1413
 
1403
1414
  let stashes: Map<string, GitStashCommit> | undefined;
1404
1415
  let stdin: string | undefined;
@@ -1431,9 +1442,22 @@ export class CommitsGitSubProvider implements GitCommitsSubProvider {
1431
1442
  } else if (!filters.refs) {
1432
1443
  // Don't include stashes when using ref: filter, as they would add unrelated commits
1433
1444
  // There *HAS* to be a better way to get git log to return stashes, but this is the best we've found
1434
- ({ stdin, stashes } = convertStashesToStdin(
1445
+ const converted = convertStashesToStdin(
1446
+ await this.provider.stash?.getStash(repoPath, undefined, cancellation),
1447
+ );
1448
+ stashes = converted.stashes;
1449
+ // `--all` already surfaces refs/stash's tip on its own, so when merges-only there's no need
1450
+ // to walk the rest of the stash stack via stdin -- parseCommits still excludes any stash
1451
+ // sha that slips through via refs/stash
1452
+ stdin = mergesOnly ? undefined : converted.stdin;
1453
+ } else if (mergesOnly) {
1454
+ // `ref:` skips walking stashes into the results (they're unrelated to the ref), but a
1455
+ // stash's tip can still leak in via `--merges` if the ref reaches refs/stash -- fetch
1456
+ // stashes here purely so parseCommits can exclude them, without widening the walk via
1457
+ // stdin
1458
+ stashes = convertStashesToStdin(
1435
1459
  await this.provider.stash?.getStash(repoPath, undefined, cancellation),
1436
- ));
1460
+ ).stashes;
1437
1461
  }
1438
1462
 
1439
1463
  if (stdin) {
@@ -1481,6 +1505,7 @@ export class CommitsGitSubProvider implements GitCommitsSubProvider {
1481
1505
  stashes,
1482
1506
  currentUser,
1483
1507
  filters,
1508
+ mergesOnly,
1484
1509
  );
1485
1510
 
1486
1511
  const log: GitLog = {
@@ -1710,6 +1735,11 @@ async function parseCommits(
1710
1735
  stashes: Map<string, GitStashCommit> | undefined,
1711
1736
  currentUser: GitUser | undefined,
1712
1737
  searchFilters?: SearchQueryFilters,
1738
+ // Stash commits have 2-3 parents, so `--merges` matches them; excluded below. Passed in by the
1739
+ // caller (derived from the args git actually runs) rather than re-derived from `searchFilters.type`
1740
+ // here, since a multi-value `type:` query (e.g. `type:merge type:tip`) leaves `--merges` in the
1741
+ // args while `searchFilters.type` reflects only the last value parsed
1742
+ mergesOnly: boolean = false,
1713
1743
  ): Promise<{ commits: Map<string, GitCommit>; count: number; countStashChildCommits: number }> {
1714
1744
  let count = 0;
1715
1745
  let countStashChildCommits = 0;
@@ -1729,6 +1759,7 @@ async function parseCommits(
1729
1759
 
1730
1760
  for (const c of parser.parse(result.stdout)) {
1731
1761
  if (stashesOnly && !stashes?.has(c.sha)) continue;
1762
+ if (mergesOnly && stashes?.has(c.sha)) continue;
1732
1763
  if (tipsOnly && !c.tips) continue;
1733
1764
 
1734
1765
  count++;
@@ -1781,6 +1812,7 @@ async function parseCommits(
1781
1812
 
1782
1813
  for await (const c of parser.parseAsync(resultOrStream)) {
1783
1814
  if (stashesOnly && !stashes?.has(c.sha)) continue;
1815
+ if (mergesOnly && stashes?.has(c.sha)) continue;
1784
1816
  if (tipsOnly && !c.tips) continue;
1785
1817
 
1786
1818
  count++;
@@ -36,7 +36,7 @@ const gkConfigAllPattern = '.';
36
36
  * section and variable-name lowercased and only the subsection case-preserved. Without this a
37
37
  * subsection-less key like `gk.defaultRemote` would miss the lowercased `gk.defaultremote` in the map.
38
38
  */
39
- function canonicalizeGitConfigKey(key: string): string {
39
+ export function canonicalizeGitConfigKey(key: string): string {
40
40
  const first = key.indexOf('.');
41
41
  if (first === -1) return key.toLowerCase();
42
42
 
@@ -50,8 +50,22 @@ function canonicalizeGitConfigKey(key: string): string {
50
50
  /**
51
51
  * Parses git config --get-regex output into a Map.
52
52
  * The output format is "key value" per line, where key and value are space-separated.
53
+ *
54
+ * A line with NO space at all is git's rendering of a bareword/valueless entry (`key`, no `=`) — git's
55
+ * OWN config parser reads that as boolean TRUE (git-config(1): "a variable defined without `= <value>`
56
+ * is taken as true"), confirmed against git's `format_config` (builtin/config.c): the key-delimiter is
57
+ * appended unconditionally, then backed out ONLY when the stored value is NULL (the bareword case) —
58
+ * an explicit empty value (`key =`) is a real, non-NULL empty string, so its line KEEPS the trailing
59
+ * space and is already handled by the branch below, byte-distinct from a bareword line.
60
+ *
61
+ * By default a bareword line is skipped (the historical behavior, which reads as "unset" — wrong, but
62
+ * safe); pass `includeValueless` to map it to the literal string `'true'` instead, so `parseGitBoolean`
63
+ * and callers checking `.has()` for "configured" both read it correctly.
53
64
  */
54
- function parseConfigRegexOutput(data: string | undefined): Map<string, string> {
65
+ export function parseConfigRegexOutput(
66
+ data: string | undefined,
67
+ options?: { includeValueless?: boolean },
68
+ ): Map<string, string> {
55
69
  const configMap = new Map<string, string>();
56
70
  if (!data) return configMap;
57
71
 
@@ -59,7 +73,12 @@ function parseConfigRegexOutput(data: string | undefined): Map<string, string> {
59
73
  if (!line) continue;
60
74
 
61
75
  const spaceIndex = line.indexOf(' ');
62
- if (spaceIndex === -1) continue;
76
+ if (spaceIndex === -1) {
77
+ if (options?.includeValueless) {
78
+ configMap.set(line, 'true');
79
+ }
80
+ continue;
81
+ }
63
82
 
64
83
  configMap.set(line.substring(0, spaceIndex), line.substring(spaceIndex + 1));
65
84
  }
@@ -67,13 +86,21 @@ function parseConfigRegexOutput(data: string | undefined): Map<string, string> {
67
86
  return configMap;
68
87
  }
69
88
 
70
- function parseGitBoolean(value: string | undefined): boolean {
89
+ export function parseGitBoolean(value: string | undefined): boolean {
71
90
  if (value == null) return false;
72
91
 
73
92
  const normalized = value.toLowerCase().trim();
74
93
  return normalized === 'true' || normalized === 'yes' || normalized === 'on' || normalized === '1';
75
94
  }
76
95
 
96
+ /** Whether a git config value spells boolean-false (`false`/`no`/`off`/`0`/empty), per git's maybe-bool parsing. */
97
+ export function isGitBooleanFalse(value: string): boolean {
98
+ const normalized = value.toLowerCase().trim();
99
+ return (
100
+ normalized === '' || normalized === 'false' || normalized === 'no' || normalized === 'off' || normalized === '0'
101
+ );
102
+ }
103
+
77
104
  export class ConfigGitSubProvider implements GitConfigSubProvider {
78
105
  constructor(
79
106
  private readonly context: GitServiceContext,
@@ -163,7 +190,10 @@ export class ConfigGitSubProvider implements GitConfigSubProvider {
163
190
  { cwd: repoPath ?? '', errors: 'ignore', runLocally: options?.runGitLocally },
164
191
  ...args,
165
192
  );
166
- return parseConfigRegexOutput(result.stdout.trim());
193
+ // Deliberately NOT `.trim()`-ed: an explicit-empty value's LAST line ends in a meaningful trailing
194
+ // space, and `.trim()`-ing the whole (possibly multi-line) buffer strips it — `parseConfigRegexOutput`
195
+ // already skips the blank line a trailing newline produces, so no trim is needed.
196
+ return parseConfigRegexOutput(result.stdout);
167
197
  }
168
198
 
169
199
  @debug()
@@ -400,7 +430,9 @@ export class ConfigGitSubProvider implements GitConfigSubProvider {
400
430
  // `--get-regex` exits 1 when nothing matches, which IS an answer (an empty gk config).
401
431
  const exited = result.completion.status === 'exited';
402
432
  return {
403
- map: parseConfigRegexOutput(result.stdout.trim()),
433
+ // Deliberately NOT `.trim()`-ed — see `getConfigRegexCore`'s comment: it strips the trailing
434
+ // space that marks an explicit-empty value's LAST line, silently dropping it from the map.
435
+ map: parseConfigRegexOutput(result.stdout),
404
436
  failed: !exited || (result.exitCode !== 0 && result.exitCode !== 1),
405
437
  };
406
438
  }