@gitkraken/core-gitlens 0.5.110 → 0.5.111

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 (362) hide show
  1. package/CHANGELOG.md +16 -1
  2. package/dist/git/cache.d.ts +111 -6
  3. package/dist/git/cache.d.ts.map +1 -1
  4. package/dist/git/cache.js +312 -43
  5. package/dist/git/cache.js.map +1 -1
  6. package/dist/git/context.d.ts +11 -0
  7. package/dist/git/context.d.ts.map +1 -1
  8. package/dist/git/context.js.map +1 -1
  9. package/dist/git/models/fileStatus.d.ts +1 -0
  10. package/dist/git/models/fileStatus.d.ts.map +1 -1
  11. package/dist/git/models/fileStatus.js +1 -0
  12. package/dist/git/models/fileStatus.js.map +1 -1
  13. package/dist/git/models/issue.d.ts +4 -0
  14. package/dist/git/models/issue.d.ts.map +1 -1
  15. package/dist/git/models/mergeConflicts.d.ts +2 -0
  16. package/dist/git/models/mergeConflicts.d.ts.map +1 -1
  17. package/dist/git/models/pullRequest.d.ts +38 -2
  18. package/dist/git/models/pullRequest.d.ts.map +1 -1
  19. package/dist/git/models/pullRequest.js +12 -1
  20. package/dist/git/models/pullRequest.js.map +1 -1
  21. package/dist/git/models/repository.d.ts +32 -5
  22. package/dist/git/models/repository.d.ts.map +1 -1
  23. package/dist/git/models/repository.js +58 -12
  24. package/dist/git/models/repository.js.map +1 -1
  25. package/dist/git/models/statusFile.d.ts.map +1 -1
  26. package/dist/git/models/statusFile.js +3 -0
  27. package/dist/git/models/statusFile.js.map +1 -1
  28. package/dist/git/providers/branches.d.ts +2 -0
  29. package/dist/git/providers/branches.d.ts.map +1 -1
  30. package/dist/git/providers/commits.d.ts +8 -0
  31. package/dist/git/providers/commits.d.ts.map +1 -1
  32. package/dist/git/providers/config.d.ts +19 -1
  33. package/dist/git/providers/config.d.ts.map +1 -1
  34. package/dist/git/providers/operations.d.ts +1 -0
  35. package/dist/git/providers/operations.d.ts.map +1 -1
  36. package/dist/git/providers/pausedOperations.d.ts +2 -0
  37. package/dist/git/providers/pausedOperations.d.ts.map +1 -1
  38. package/dist/git/providers/status.d.ts +1 -0
  39. package/dist/git/providers/status.d.ts.map +1 -1
  40. package/dist/git/utils/config.utils.d.ts +3 -0
  41. package/dist/git/utils/config.utils.d.ts.map +1 -0
  42. package/dist/git/utils/config.utils.js +8 -0
  43. package/dist/git/utils/config.utils.js.map +1 -0
  44. package/dist/git/utils/issue.utils.d.ts.map +1 -1
  45. package/dist/git/utils/issue.utils.js +2 -0
  46. package/dist/git/utils/issue.utils.js.map +1 -1
  47. package/dist/git/utils/pausedOperationStatus.utils.d.ts +14 -0
  48. package/dist/git/utils/pausedOperationStatus.utils.d.ts.map +1 -1
  49. package/dist/git/utils/pausedOperationStatus.utils.js +23 -0
  50. package/dist/git/utils/pausedOperationStatus.utils.js.map +1 -1
  51. package/dist/git/utils/pullRequest.utils.d.ts +14 -1
  52. package/dist/git/utils/pullRequest.utils.d.ts.map +1 -1
  53. package/dist/git/utils/pullRequest.utils.js +31 -0
  54. package/dist/git/utils/pullRequest.utils.js.map +1 -1
  55. package/dist/git/watching/changeEvent.d.ts +6 -1
  56. package/dist/git/watching/changeEvent.d.ts.map +1 -1
  57. package/dist/git/watching/classifyChange.d.ts.map +1 -1
  58. package/dist/git/watching/classifyChange.js +14 -1
  59. package/dist/git/watching/classifyChange.js.map +1 -1
  60. package/dist/git/watching/watchService.d.ts.map +1 -1
  61. package/dist/git/watching/watchService.js +18 -3
  62. package/dist/git/watching/watchService.js.map +1 -1
  63. package/dist/git/watching/watchSession.d.ts +8 -1
  64. package/dist/git/watching/watchSession.d.ts.map +1 -1
  65. package/dist/git/watching/watchSession.js +38 -6
  66. package/dist/git/watching/watchSession.js.map +1 -1
  67. package/dist/git/watching/watcherPatterns.d.ts +2 -2
  68. package/dist/git/watching/watcherPatterns.d.ts.map +1 -1
  69. package/dist/git/watching/watcherPatterns.js +1 -1
  70. package/dist/git/watching/watcherPatterns.js.map +1 -1
  71. package/dist/git-cli/exec/git.d.ts +1 -1
  72. package/dist/git-cli/exec/git.d.ts.map +1 -1
  73. package/dist/git-cli/exec/git.js +13 -3
  74. package/dist/git-cli/exec/git.js.map +1 -1
  75. package/dist/git-cli/providers/branches.d.ts +19 -0
  76. package/dist/git-cli/providers/branches.d.ts.map +1 -1
  77. package/dist/git-cli/providers/branches.js +282 -78
  78. package/dist/git-cli/providers/branches.js.map +1 -1
  79. package/dist/git-cli/providers/commits.d.ts +4 -0
  80. package/dist/git-cli/providers/commits.d.ts.map +1 -1
  81. package/dist/git-cli/providers/commits.js +84 -11
  82. package/dist/git-cli/providers/commits.js.map +1 -1
  83. package/dist/git-cli/providers/config.d.ts +42 -0
  84. package/dist/git-cli/providers/config.d.ts.map +1 -1
  85. package/dist/git-cli/providers/config.js +186 -5
  86. package/dist/git-cli/providers/config.js.map +1 -1
  87. package/dist/git-cli/providers/contributors.d.ts.map +1 -1
  88. package/dist/git-cli/providers/contributors.js +1 -1
  89. package/dist/git-cli/providers/contributors.js.map +1 -1
  90. package/dist/git-cli/providers/graph.d.ts +2 -2
  91. package/dist/git-cli/providers/graph.d.ts.map +1 -1
  92. package/dist/git-cli/providers/graph.js +27 -9
  93. package/dist/git-cli/providers/graph.js.map +1 -1
  94. package/dist/git-cli/providers/operations.d.ts.map +1 -1
  95. package/dist/git-cli/providers/operations.js +40 -12
  96. package/dist/git-cli/providers/operations.js.map +1 -1
  97. package/dist/git-cli/providers/pausedOperations.d.ts +5 -0
  98. package/dist/git-cli/providers/pausedOperations.d.ts.map +1 -1
  99. package/dist/git-cli/providers/pausedOperations.js +87 -16
  100. package/dist/git-cli/providers/pausedOperations.js.map +1 -1
  101. package/dist/git-cli/providers/refs.d.ts +5 -3
  102. package/dist/git-cli/providers/refs.d.ts.map +1 -1
  103. package/dist/git-cli/providers/refs.js +33 -4
  104. package/dist/git-cli/providers/refs.js.map +1 -1
  105. package/dist/git-cli/providers/status.d.ts +1 -0
  106. package/dist/git-cli/providers/status.d.ts.map +1 -1
  107. package/dist/git-cli/providers/status.js +18 -3
  108. package/dist/git-cli/providers/status.js.map +1 -1
  109. package/dist/git-cli/providers/worktrees.d.ts.map +1 -1
  110. package/dist/git-cli/providers/worktrees.js +4 -0
  111. package/dist/git-cli/providers/worktrees.js.map +1 -1
  112. package/dist/git-cli/service.d.ts.map +1 -1
  113. package/dist/git-cli/service.js.map +1 -1
  114. package/dist/plus/agents/providers/claudeCodeProvider.d.ts +39 -1
  115. package/dist/plus/agents/providers/claudeCodeProvider.d.ts.map +1 -1
  116. package/dist/plus/agents/providers/claudeCodeProvider.js +978 -71
  117. package/dist/plus/agents/providers/claudeCodeProvider.js.map +1 -1
  118. package/dist/plus/agents/providers/claudeCodeTranscript.d.ts +24 -2
  119. package/dist/plus/agents/providers/claudeCodeTranscript.d.ts.map +1 -1
  120. package/dist/plus/agents/providers/claudeCodeTranscript.js +84 -9
  121. package/dist/plus/agents/providers/claudeCodeTranscript.js.map +1 -1
  122. package/dist/plus/agents/types.d.ts +37 -2
  123. package/dist/plus/agents/types.d.ts.map +1 -1
  124. package/dist/plus/agents/types.js +2 -0
  125. package/dist/plus/agents/types.js.map +1 -1
  126. package/dist/plus/ai/errors.d.ts +10 -0
  127. package/dist/plus/ai/errors.d.ts.map +1 -1
  128. package/dist/plus/ai/errors.js +29 -0
  129. package/dist/plus/ai/errors.js.map +1 -1
  130. package/dist/plus/ai/models/model.d.ts +4 -0
  131. package/dist/plus/ai/models/model.d.ts.map +1 -1
  132. package/dist/plus/ai/models/model.js +51 -6
  133. package/dist/plus/ai/models/model.js.map +1 -1
  134. package/dist/plus/ai/models/promptTemplates.d.ts +1 -1
  135. package/dist/plus/ai/models/promptTemplates.d.ts.map +1 -1
  136. package/dist/plus/ai/models/provider.d.ts +60 -2
  137. package/dist/plus/ai/models/provider.d.ts.map +1 -1
  138. package/dist/plus/ai/prompts.d.ts +12 -0
  139. package/dist/plus/ai/prompts.d.ts.map +1 -1
  140. package/dist/plus/ai/prompts.js +177 -61
  141. package/dist/plus/ai/prompts.js.map +1 -1
  142. package/dist/plus/ai/providers/anthropicProvider.d.ts +10 -2
  143. package/dist/plus/ai/providers/anthropicProvider.d.ts.map +1 -1
  144. package/dist/plus/ai/providers/anthropicProvider.js +139 -46
  145. package/dist/plus/ai/providers/anthropicProvider.js.map +1 -1
  146. package/dist/plus/ai/providers/deepSeekProvider.d.ts +2 -0
  147. package/dist/plus/ai/providers/deepSeekProvider.d.ts.map +1 -1
  148. package/dist/plus/ai/providers/deepSeekProvider.js +10 -21
  149. package/dist/plus/ai/providers/deepSeekProvider.js.map +1 -1
  150. package/dist/plus/ai/providers/geminiProvider.d.ts +1 -0
  151. package/dist/plus/ai/providers/geminiProvider.d.ts.map +1 -1
  152. package/dist/plus/ai/providers/geminiProvider.js +19 -0
  153. package/dist/plus/ai/providers/geminiProvider.js.map +1 -1
  154. package/dist/plus/ai/providers/gitkrakenProvider.d.ts +5 -1
  155. package/dist/plus/ai/providers/gitkrakenProvider.d.ts.map +1 -1
  156. package/dist/plus/ai/providers/gitkrakenProvider.js +118 -3
  157. package/dist/plus/ai/providers/gitkrakenProvider.js.map +1 -1
  158. package/dist/plus/ai/providers/huggingFaceProvider.d.ts +1 -0
  159. package/dist/plus/ai/providers/huggingFaceProvider.d.ts.map +1 -1
  160. package/dist/plus/ai/providers/huggingFaceProvider.js +5 -0
  161. package/dist/plus/ai/providers/huggingFaceProvider.js.map +1 -1
  162. package/dist/plus/ai/providers/mistralProvider.d.ts +3 -1
  163. package/dist/plus/ai/providers/mistralProvider.d.ts.map +1 -1
  164. package/dist/plus/ai/providers/mistralProvider.js +99 -19
  165. package/dist/plus/ai/providers/mistralProvider.js.map +1 -1
  166. package/dist/plus/ai/providers/ollamaProvider.d.ts +4 -3
  167. package/dist/plus/ai/providers/ollamaProvider.d.ts.map +1 -1
  168. package/dist/plus/ai/providers/ollamaProvider.js +38 -3
  169. package/dist/plus/ai/providers/ollamaProvider.js.map +1 -1
  170. package/dist/plus/ai/providers/openAICompatibleProvider.d.ts +1 -0
  171. package/dist/plus/ai/providers/openAICompatibleProvider.d.ts.map +1 -1
  172. package/dist/plus/ai/providers/openAICompatibleProvider.js +4 -0
  173. package/dist/plus/ai/providers/openAICompatibleProvider.js.map +1 -1
  174. package/dist/plus/ai/providers/openAICompatibleProviderBase.d.ts +154 -5
  175. package/dist/plus/ai/providers/openAICompatibleProviderBase.d.ts.map +1 -1
  176. package/dist/plus/ai/providers/openAICompatibleProviderBase.js +259 -10
  177. package/dist/plus/ai/providers/openAICompatibleProviderBase.js.map +1 -1
  178. package/dist/plus/ai/providers/openRouterProvider.d.ts +4 -0
  179. package/dist/plus/ai/providers/openRouterProvider.d.ts.map +1 -1
  180. package/dist/plus/ai/providers/openRouterProvider.js +9 -0
  181. package/dist/plus/ai/providers/openRouterProvider.js.map +1 -1
  182. package/dist/plus/ai/providers/openaiProvider.d.ts +1 -0
  183. package/dist/plus/ai/providers/openaiProvider.d.ts.map +1 -1
  184. package/dist/plus/ai/providers/openaiProvider.js +1 -0
  185. package/dist/plus/ai/providers/openaiProvider.js.map +1 -1
  186. package/dist/plus/ai/providers/responseFormatCache.d.ts +8 -0
  187. package/dist/plus/ai/providers/responseFormatCache.d.ts.map +1 -0
  188. package/dist/plus/ai/providers/responseFormatCache.js +25 -0
  189. package/dist/plus/ai/providers/responseFormatCache.js.map +1 -0
  190. package/dist/plus/ai/providers/xaiProvider.d.ts +1 -0
  191. package/dist/plus/ai/providers/xaiProvider.d.ts.map +1 -1
  192. package/dist/plus/ai/providers/xaiProvider.js +10 -1
  193. package/dist/plus/ai/providers/xaiProvider.js.map +1 -1
  194. package/dist/plus/ai/utils/ai.utils.d.ts +3 -0
  195. package/dist/plus/ai/utils/ai.utils.d.ts.map +1 -1
  196. package/dist/plus/ai/utils/ai.utils.js +13 -0
  197. package/dist/plus/ai/utils/ai.utils.js.map +1 -1
  198. package/dist/plus/ai/utils/results.utils.d.ts +16 -2
  199. package/dist/plus/ai/utils/results.utils.d.ts.map +1 -1
  200. package/dist/plus/ai/utils/results.utils.js +204 -28
  201. package/dist/plus/ai/utils/results.utils.js.map +1 -1
  202. package/dist/plus/git-github/api/github.d.ts +55 -0
  203. package/dist/plus/git-github/api/github.d.ts.map +1 -1
  204. package/dist/plus/git-github/api/github.js +177 -4
  205. package/dist/plus/git-github/api/github.js.map +1 -1
  206. package/dist/plus/git-github/models.d.ts +16 -0
  207. package/dist/plus/git-github/models.d.ts.map +1 -1
  208. package/dist/plus/git-github/models.js +29 -3
  209. package/dist/plus/git-github/models.js.map +1 -1
  210. package/dist/plus/git-github/providers/github/status.d.ts +1 -0
  211. package/dist/plus/git-github/providers/github/status.d.ts.map +1 -1
  212. package/dist/plus/git-github/providers/github/status.js.map +1 -1
  213. package/dist/plus/integrations/context.d.ts +4 -0
  214. package/dist/plus/integrations/context.d.ts.map +1 -1
  215. package/dist/plus/integrations/index.d.ts.map +1 -1
  216. package/dist/plus/integrations/index.js +2 -0
  217. package/dist/plus/integrations/index.js.map +1 -1
  218. package/dist/plus/integrations/models/gitHostIntegration.d.ts +6 -3
  219. package/dist/plus/integrations/models/gitHostIntegration.d.ts.map +1 -1
  220. package/dist/plus/integrations/models/gitHostIntegration.js +12 -2
  221. package/dist/plus/integrations/models/gitHostIntegration.js.map +1 -1
  222. package/dist/plus/integrations/models/integration.d.ts +3 -1
  223. package/dist/plus/integrations/models/integration.d.ts.map +1 -1
  224. package/dist/plus/integrations/models/integration.js +2 -2
  225. package/dist/plus/integrations/models/integration.js.map +1 -1
  226. package/dist/plus/integrations/providers/accounts.d.ts.map +1 -1
  227. package/dist/plus/integrations/providers/accounts.js +4 -0
  228. package/dist/plus/integrations/providers/accounts.js.map +1 -1
  229. package/dist/plus/integrations/providers/azure/models.d.ts.map +1 -1
  230. package/dist/plus/integrations/providers/azure/models.js +3 -0
  231. package/dist/plus/integrations/providers/azure/models.js.map +1 -1
  232. package/dist/plus/integrations/providers/bitbucket/models.d.ts.map +1 -1
  233. package/dist/plus/integrations/providers/bitbucket/models.js +1 -0
  234. package/dist/plus/integrations/providers/bitbucket/models.js.map +1 -1
  235. package/dist/plus/integrations/providers/github.d.ts +14 -2
  236. package/dist/plus/integrations/providers/github.d.ts.map +1 -1
  237. package/dist/plus/integrations/providers/github.js +55 -3
  238. package/dist/plus/integrations/providers/github.js.map +1 -1
  239. package/dist/plus/integrations/providers/gitlab.d.ts.map +1 -1
  240. package/dist/plus/integrations/providers/gitlab.js.map +1 -1
  241. package/dist/plus/integrations/providers/models.d.ts.map +1 -1
  242. package/dist/plus/integrations/providers/models.js +30 -4
  243. package/dist/plus/integrations/providers/models.js.map +1 -1
  244. package/dist/plus/integrations/reads/broaden.d.ts.map +1 -1
  245. package/dist/plus/integrations/reads/broaden.js +6 -1
  246. package/dist/plus/integrations/reads/broaden.js.map +1 -1
  247. package/dist/plus/integrations/reads/drains.d.ts.map +1 -1
  248. package/dist/plus/integrations/reads/drains.js +7 -4
  249. package/dist/plus/integrations/reads/drains.js.map +1 -1
  250. package/dist/plus/integrations/reads/hierarchy.d.ts.map +1 -1
  251. package/dist/plus/integrations/reads/hierarchy.js +3 -1
  252. package/dist/plus/integrations/reads/hierarchy.js.map +1 -1
  253. package/dist/plus/integrations/reads/issueTracker.d.ts.map +1 -1
  254. package/dist/plus/integrations/reads/issueTracker.js +8 -3
  255. package/dist/plus/integrations/reads/issueTracker.js.map +1 -1
  256. package/dist/plus/integrations/reads/issues.d.ts.map +1 -1
  257. package/dist/plus/integrations/reads/issues.js +9 -4
  258. package/dist/plus/integrations/reads/issues.js.map +1 -1
  259. package/dist/plus/integrations/reads/pullRequests.d.ts.map +1 -1
  260. package/dist/plus/integrations/reads/pullRequests.js +9 -4
  261. package/dist/plus/integrations/reads/pullRequests.js.map +1 -1
  262. package/dist/utils/env/browser/base64.d.ts +1 -1
  263. package/dist/utils/env/browser/base64.d.ts.map +1 -1
  264. package/dist/utils/env/node/base64.d.ts +1 -1
  265. package/dist/utils/env/node/base64.d.ts.map +1 -1
  266. package/dist/utils/env/node/base64.js +2 -0
  267. package/dist/utils/env/node/base64.js.map +1 -1
  268. package/dist/utils/keys/chord.d.ts +52 -0
  269. package/dist/utils/keys/chord.d.ts.map +1 -0
  270. package/dist/utils/keys/chord.js +135 -0
  271. package/dist/utils/keys/chord.js.map +1 -0
  272. package/dist/utils/keys/keybinding.d.ts +53 -0
  273. package/dist/utils/keys/keybinding.d.ts.map +1 -0
  274. package/dist/utils/keys/keybinding.js +58 -0
  275. package/dist/utils/keys/keybinding.js.map +1 -0
  276. package/dist/utils/path.d.ts +11 -0
  277. package/dist/utils/path.d.ts.map +1 -1
  278. package/dist/utils/path.js +13 -0
  279. package/dist/utils/path.js.map +1 -1
  280. package/package.json +13 -5
  281. package/src/git/cache.ts +383 -47
  282. package/src/git/context.ts +11 -0
  283. package/src/git/models/fileStatus.ts +1 -0
  284. package/src/git/models/issue.ts +4 -0
  285. package/src/git/models/mergeConflicts.ts +3 -2
  286. package/src/git/models/pullRequest.ts +33 -1
  287. package/src/git/models/repository.ts +58 -17
  288. package/src/git/models/statusFile.ts +3 -0
  289. package/src/git/providers/branches.ts +6 -0
  290. package/src/git/providers/commits.ts +11 -0
  291. package/src/git/providers/config.ts +18 -0
  292. package/src/git/providers/operations.ts +1 -0
  293. package/src/git/providers/pausedOperations.ts +2 -0
  294. package/src/git/providers/status.ts +1 -0
  295. package/src/git/utils/config.utils.ts +8 -0
  296. package/src/git/utils/issue.utils.ts +2 -0
  297. package/src/git/utils/pausedOperationStatus.utils.ts +28 -0
  298. package/src/git/utils/pullRequest.utils.ts +36 -0
  299. package/src/git/watching/changeEvent.ts +6 -1
  300. package/src/git/watching/classifyChange.ts +15 -1
  301. package/src/git/watching/watchService.ts +20 -3
  302. package/src/git/watching/watchSession.ts +41 -6
  303. package/src/git/watching/watcherPatterns.ts +1 -1
  304. package/src/git-cli/exec/git.ts +13 -3
  305. package/src/git-cli/providers/branches.ts +384 -132
  306. package/src/git-cli/providers/commits.ts +106 -15
  307. package/src/git-cli/providers/config.ts +229 -8
  308. package/src/git-cli/providers/contributors.ts +5 -1
  309. package/src/git-cli/providers/graph.ts +26 -13
  310. package/src/git-cli/providers/operations.ts +45 -20
  311. package/src/git-cli/providers/pausedOperations.ts +109 -17
  312. package/src/git-cli/providers/refs.ts +33 -3
  313. package/src/git-cli/providers/status.ts +25 -4
  314. package/src/git-cli/providers/worktrees.ts +5 -0
  315. package/src/git-cli/service.ts +0 -1
  316. package/src/plus/agents/providers/claudeCodeProvider.ts +1130 -87
  317. package/src/plus/agents/providers/claudeCodeTranscript.ts +95 -9
  318. package/src/plus/agents/types.ts +46 -2
  319. package/src/plus/ai/errors.ts +30 -0
  320. package/src/plus/ai/models/model.ts +56 -6
  321. package/src/plus/ai/models/promptTemplates.ts +1 -1
  322. package/src/plus/ai/models/provider.ts +69 -2
  323. package/src/plus/ai/prompts.ts +186 -61
  324. package/src/plus/ai/providers/anthropicProvider.ts +155 -47
  325. package/src/plus/ai/providers/deepSeekProvider.ts +11 -21
  326. package/src/plus/ai/providers/geminiProvider.ts +19 -0
  327. package/src/plus/ai/providers/gitkrakenProvider.ts +126 -3
  328. package/src/plus/ai/providers/huggingFaceProvider.ts +6 -0
  329. package/src/plus/ai/providers/mistralProvider.ts +102 -19
  330. package/src/plus/ai/providers/ollamaProvider.ts +55 -4
  331. package/src/plus/ai/providers/openAICompatibleProvider.ts +5 -0
  332. package/src/plus/ai/providers/openAICompatibleProviderBase.ts +360 -17
  333. package/src/plus/ai/providers/openRouterProvider.ts +17 -0
  334. package/src/plus/ai/providers/openaiProvider.ts +1 -0
  335. package/src/plus/ai/providers/responseFormatCache.ts +39 -0
  336. package/src/plus/ai/providers/xaiProvider.ts +10 -1
  337. package/src/plus/ai/utils/ai.utils.ts +14 -0
  338. package/src/plus/ai/utils/results.utils.ts +255 -31
  339. package/src/plus/git-github/api/github.ts +291 -7
  340. package/src/plus/git-github/models.ts +48 -2
  341. package/src/plus/git-github/providers/github/status.ts +1 -0
  342. package/src/plus/integrations/context.ts +4 -0
  343. package/src/plus/integrations/index.ts +2 -0
  344. package/src/plus/integrations/models/gitHostIntegration.ts +25 -2
  345. package/src/plus/integrations/models/integration.ts +23 -13
  346. package/src/plus/integrations/providers/accounts.ts +4 -0
  347. package/src/plus/integrations/providers/azure/models.ts +3 -0
  348. package/src/plus/integrations/providers/bitbucket/models.ts +1 -0
  349. package/src/plus/integrations/providers/github.ts +85 -1
  350. package/src/plus/integrations/providers/gitlab.ts +2 -10
  351. package/src/plus/integrations/providers/models.ts +36 -4
  352. package/src/plus/integrations/reads/broaden.ts +6 -1
  353. package/src/plus/integrations/reads/drains.ts +7 -4
  354. package/src/plus/integrations/reads/hierarchy.ts +3 -1
  355. package/src/plus/integrations/reads/issueTracker.ts +9 -3
  356. package/src/plus/integrations/reads/issues.ts +9 -4
  357. package/src/plus/integrations/reads/pullRequests.ts +9 -4
  358. package/src/utils/env/browser/base64.ts +1 -1
  359. package/src/utils/env/node/base64.ts +3 -1
  360. package/src/utils/keys/chord.ts +184 -0
  361. package/src/utils/keys/keybinding.ts +116 -0
  362. package/src/utils/path.ts +14 -0
@@ -3,7 +3,7 @@ import type { GitServiceContext } from '../../git/context.js';
3
3
  import { BranchError } from '../../git/errors.js';
4
4
  import type { BranchDisposition, BranchMetadata } from '../../git/models/branch.js';
5
5
  import { GitBranch } from '../../git/models/branch.js';
6
- import type { ConflictDetectionResult } from '../../git/models/mergeConflicts.js';
6
+ import type { ConflictDetectionErrorReason, ConflictDetectionResult } from '../../git/models/mergeConflicts.js';
7
7
  import type { GitBranchReference } from '../../git/models/reference.js';
8
8
  import type {
9
9
  BranchContributionsOverview,
@@ -41,6 +41,7 @@ import type { CliGitProviderInternal } from '../cliGitProvider.js';
41
41
  import type { GitResult } from '../exec/exec.types.js';
42
42
  import type { Git } from '../exec/git.js';
43
43
  import { getGitCommandError, gitConfigsBranch, gitConfigsLog, GitError, GitErrors, GitWarnings } from '../exec/git.js';
44
+ import type { GitMergeConflict } from '../parsers/mergeTreeParser.js';
44
45
  import { parseMergeTreeConflict } from '../parsers/mergeTreeParser.js';
45
46
 
46
47
  /** Minimum time between writes to the config for last accessed/modified dates */
@@ -184,6 +185,14 @@ export class BranchesGitSubProvider implements GitBranchesSubProvider {
184
185
 
185
186
  // If we don't get any data, assume the repo doesn't have any commits yet so check if we have a current branch
186
187
  if (!records.length) {
188
+ // Never CACHE that conclusion. `getRefs` reports a failed enumeration the same way as a
189
+ // genuinely ref-less repo — an empty array (it invalidates its own entry but doesn't
190
+ // throw) — so a transient `for-each-ref` failure would otherwise pin this degraded
191
+ // current-branch-only list in a map with no TTL, and every branch consumer would see a
192
+ // one-branch repo for the session. A truly empty repo re-reads cheaply, so refusing the
193
+ // entry costs nothing in the case this is actually meant to serve.
194
+ cacheable?.invalidate();
195
+
187
196
  const current = await this.getCurrentBranch(
188
197
  commonPath,
189
198
  metadataMap,
@@ -360,7 +369,9 @@ export class BranchesGitSubProvider implements GitBranchesSubProvider {
360
369
  repoPath: string,
361
370
  ref: string,
362
371
  options?: {
363
- associatedPullRequest?: Promise<{ refs?: { base?: { branch: string } } } | undefined>;
372
+ associatedPullRequest?: Promise<
373
+ { refs?: { base?: { branch: string } }; stack?: { position: number } } | undefined
374
+ >;
364
375
  priority?: GitCommandPriority;
365
376
  },
366
377
  cancellation?: AbortSignal,
@@ -478,14 +489,20 @@ export class BranchesGitSubProvider implements GitBranchesSubProvider {
478
489
  if (branch == null) return undefined;
479
490
 
480
491
  mergeTarget = `${branch.remoteName}/${pr.refs.base.branch}`;
481
- // Store for future reuse turns the next call into a Tier 1 path. Skip the
482
- // `branchOverviews` invalidation that the write would otherwise trigger: we're
483
- // about to populate `${ref}|${mergeTarget}` with this exact target, so evicting
484
- // it would just force the next caller to redo the body. (gk-merge-target writes
485
- // do not affect `baseBranchName`, so no need to skip that target.)
486
- void this.storeMergeTargetBranchName(repoPath, ref, mergeTarget, {
487
- skipInvalidation: ['branchOverviews'],
488
- });
492
+ // A stacked PR's base is the layer below it, which is deleted when the stack merges.
493
+ // Storing it would leave a stored target naming a branch that no longer exists and
494
+ // a stored target beats every other tier, so it would keep winning until cleared by
495
+ // hand. Use it for this lookup, but don't persist it.
496
+ if (pr.stack == null) {
497
+ // Store for future reuse — turns the next call into a Tier 1 path. Skip the
498
+ // `branchOverviews` invalidation that the write would otherwise trigger: we're
499
+ // about to populate `${ref}|${mergeTarget}` with this exact target, so evicting
500
+ // it would just force the next caller to redo the body. (gk-merge-target writes
501
+ // do not affect `baseBranchName`, so no need to skip that target.)
502
+ void this.storeMergeTargetBranchName(repoPath, ref, mergeTarget, {
503
+ skipInvalidation: ['branchOverviews'],
504
+ });
505
+ }
489
506
  }
490
507
  }
491
508
 
@@ -736,74 +753,116 @@ export class BranchesGitSubProvider implements GitBranchesSubProvider {
736
753
  const priority = options?.priority;
737
754
  const priorityOpts = priority != null ? { priority: priority } : undefined;
738
755
 
739
- // Local-only: read just the existing local symref, never contacting the remote. Bypass the shared
740
- // cache entirelyit's keyed by repoPath+remote, so caching a local miss (or sharing this in-flight
741
- // promise) would starve a concurrent networked caller of the real default branch. The local read is
742
- // a cheap ref lookup, fine to repeat per call.
756
+ // Local-only: read just the existing local symref, never contacting the remote. Cached under a
757
+ // `${remote}:local` keya colon can't appear in a refname component, so it can never collide
758
+ // with a real remote name used by the networked variant below. Invalidated by the same
759
+ // remotes/branches cache cascades as the networked lookup.
743
760
  if (options?.local) {
744
- try {
745
- const result = await this.git.run(
746
- { cwd: this.cache.getCommonPath(repoPath), cancellation: cancellation, ...priorityOpts },
747
- 'symbolic-ref',
748
- '--short',
749
- `refs/remotes/${remote}/HEAD`,
750
- );
751
- return result.stdout.trim() || undefined;
752
- } catch (ex) {
753
- if (isCancellationError(ex)) throw ex;
754
- return undefined;
755
- }
761
+ return this.cache.getDefaultBranchName(
762
+ repoPath,
763
+ `${remote}:local`,
764
+ async (commonPath, cacheable, signal) => {
765
+ try {
766
+ // Bind to the aggregate `signal` (fires only when ALL current callers abort), not
767
+ // this-caller's `cancellation` — otherwise a superseded caller's abort would kill the
768
+ // shared command and reject concurrent riders that never cancelled.
769
+ const result = await this.git.run(
770
+ { cwd: commonPath, cancellation: signal ?? cancellation, ...priorityOpts },
771
+ 'symbolic-ref',
772
+ '--short',
773
+ `refs/remotes/${remote}/HEAD`,
774
+ );
775
+ return result.stdout.trim() || undefined;
776
+ } catch (ex) {
777
+ if (isCancellationError(ex)) throw ex;
778
+
779
+ // `symbolic-ref` on an absent `refs/remotes/<remote>/HEAD` exits fatal rather than
780
+ // empty, and that message matches no `GitWarnings` entry, so it arrives here as a
781
+ // throw. It is still an ANSWER — the ref genuinely isn't set — and the overwhelmingly
782
+ // common one, so cache it. Invalidating instead means every caller re-probes and the
783
+ // whole point of caching this lookup is lost.
784
+ const stderr = (ex as { stderr?: string }).stderr ?? '';
785
+ if (stderr.includes('is not a symbolic ref')) return undefined;
786
+
787
+ // Anything else never produced an answer (spawn failure, queue rejection). This entry
788
+ // has no TTL, so caching that would pin "no default branch" for the session.
789
+ cacheable.invalidate();
790
+ return undefined;
791
+ }
792
+ },
793
+ cancellation,
794
+ );
756
795
  }
757
796
 
758
- return this.cache.getDefaultBranchName(repoPath, remote, async commonPath => {
759
- let retried = false;
760
- while (true) {
761
- try {
762
- const result = await this.git.run(
763
- { cwd: commonPath, cancellation: cancellation, ...priorityOpts },
764
- 'symbolic-ref',
765
- '--short',
766
- `refs/remotes/${remote}/HEAD`,
767
- );
768
- return result.stdout.trim() || undefined;
769
- } catch (ex) {
770
- if (/is not a symbolic ref/.test(ex.stderr)) {
771
- try {
772
- if (!retried) {
773
- retried = true;
774
- await this.git.run(
775
- { cwd: commonPath, cancellation: cancellation, ...priorityOpts },
776
- 'remote',
777
- 'set-head',
778
- '-a',
797
+ return this.cache.getDefaultBranchName(
798
+ repoPath,
799
+ remote,
800
+ async (commonPath, cacheable, signal) => {
801
+ let retried = false;
802
+ while (true) {
803
+ try {
804
+ const result = await this.git.run(
805
+ { cwd: commonPath, cancellation: signal ?? cancellation, ...priorityOpts },
806
+ 'symbolic-ref',
807
+ '--short',
808
+ `refs/remotes/${remote}/HEAD`,
809
+ );
810
+ return result.stdout.trim() || undefined;
811
+ } catch (ex) {
812
+ // Only "no HEAD ref configured" is an ANSWER here; every other throw means the lookup
813
+ // never happened. This entry has no TTL, so caching a failure — an offline `ls-remote`
814
+ // being the likely one would pin "no default branch" for the session.
815
+ let answered = false;
816
+ if (/is not a symbolic ref/.test(ex.stderr)) {
817
+ try {
818
+ if (!retried) {
819
+ retried = true;
820
+ await this.git.run(
821
+ { cwd: commonPath, cancellation: signal ?? cancellation, ...priorityOpts },
822
+ 'remote',
823
+ 'set-head',
824
+ '-a',
825
+ remote,
826
+ );
827
+ // This just created `refs/remotes/<remote>/HEAD`, which the `local` variant
828
+ // may already have cached a miss for. The watcher would also catch the new
829
+ // ref, but evict directly so consumers of this package that don't wire up a
830
+ // watcher aren't stuck with that miss.
831
+ this.cache.deleteDefaultBranchName(commonPath, `${remote}:local`);
832
+ continue;
833
+ }
834
+
835
+ const result = await this.git.run(
836
+ { cwd: commonPath, cancellation: signal ?? cancellation, ...priorityOpts },
837
+ 'ls-remote',
838
+ '--symref',
779
839
  remote,
840
+ 'HEAD',
780
841
  );
781
- continue;
782
- }
783
-
784
- const result = await this.git.run(
785
- { cwd: commonPath, cancellation: cancellation, ...priorityOpts },
786
- 'ls-remote',
787
- '--symref',
788
- remote,
789
- 'HEAD',
790
- );
791
- if (result.stdout) {
792
- const match = /ref:\s(\S+)\s+HEAD/m.exec(result.stdout);
793
- if (match != null) {
794
- const [, branch] = match;
795
- return `${remote}/${branch.substring('refs/heads/'.length).trim()}`;
842
+ if (result.stdout) {
843
+ const match = /ref:\s(\S+)\s+HEAD/m.exec(result.stdout);
844
+ if (match != null) {
845
+ const [, branch] = match;
846
+ return `${remote}/${branch.substring('refs/heads/'.length).trim()}`;
847
+ }
796
848
  }
849
+
850
+ // `ls-remote` ran and the remote advertised no HEAD — a real "none".
851
+ answered = true;
852
+ } catch {
853
+ if (isCancellationError(ex)) throw ex;
797
854
  }
798
- } catch {
799
- if (isCancellationError(ex)) throw ex;
800
855
  }
801
- }
802
856
 
803
- return undefined;
857
+ if (!answered) {
858
+ cacheable.invalidate();
859
+ }
860
+ return undefined;
861
+ }
804
862
  }
805
- }
806
- });
863
+ },
864
+ cancellation,
865
+ );
807
866
  }
808
867
 
809
868
  @debug()
@@ -814,6 +873,12 @@ export class BranchesGitSubProvider implements GitBranchesSubProvider {
814
873
  }
815
874
  try {
816
875
  await this.git.run({ cwd: repoPath }, ...args);
876
+ // Evict the cached base so the new branch re-derives its own. The PERSISTED `branch.<ref>.gk-*`
877
+ // keys are left alone: they're dropped when GitLens deletes a branch, never when one is created.
878
+ // Removing them here would mean treating a reused name as proof of a different branch, which
879
+ // nothing can establish — so the trade is that a branch reusing a name deleted outside GitLens
880
+ // inherits the orphaned section.
881
+ this.cache.deleteBaseBranchName(repoPath, name);
817
882
  this.context.hooks?.cache?.onReset?.(repoPath, 'branches');
818
883
  this.context.hooks?.repository?.onChanged?.(repoPath, ['heads']);
819
884
  } catch (ex) {
@@ -840,9 +905,31 @@ export class BranchesGitSubProvider implements GitBranchesSubProvider {
840
905
  const args = ['branch', options?.force ? '-D' : '-d', ...branches];
841
906
  try {
842
907
  await this.git.run({ cwd: repoPath }, ...args);
908
+ for (const branch of branches) {
909
+ this.cache.deleteBaseBranchName(repoPath, branch);
910
+ // Drop the persisted gk metadata too, not just the cached resolution. `getBaseBranchName`
911
+ // reads `branch.<ref>.gk-merge-base` before falling back to the reflog, so leaving it behind
912
+ // would hand a later branch reusing this name its predecessor's base — evicting the cache
913
+ // alone just forces a re-derivation that reads the same stale value back.
914
+ await this.provider.config.removeGkConfigBranchSection(repoPath, branch);
915
+ }
843
916
  this.context.hooks?.cache?.onReset?.(repoPath, 'branches');
844
917
  this.context.hooks?.repository?.onChanged?.(repoPath, ['heads']);
845
918
  } catch (ex) {
919
+ // A multi-name delete partially succeeds: `git branch -d a bad c` removes `a` and `c`, refuses
920
+ // `bad`, and still exits non-zero — so the loop above never ran for the branches that did go.
921
+ // Clean those up here rather than leave persisted gk metadata behind for a name a later branch
922
+ // can reuse.
923
+ const anyDeleted = await this.cleanupDeletedBranchMetadata(repoPath, branches);
924
+
925
+ // The success path's notifications never ran, but some branches DID go — without these the
926
+ // branch caches stay stale and consumers wait on the filesystem watcher (which never fires
927
+ // for a packed ref).
928
+ if (anyDeleted) {
929
+ this.context.hooks?.cache?.onReset?.(repoPath, 'branches');
930
+ this.context.hooks?.repository?.onChanged?.(repoPath, ['heads']);
931
+ }
932
+
846
933
  if (ex instanceof BranchError) {
847
934
  throw ex.update({
848
935
  action: options?.force ? 'force delete' : 'delete',
@@ -862,6 +949,62 @@ export class BranchesGitSubProvider implements GitBranchesSubProvider {
862
949
  }
863
950
  }
864
951
 
952
+ /**
953
+ * Drops the cached and persisted per-branch metadata for whichever of `names` no longer exists. Only
954
+ * for the partial-failure path — one extra `for-each-ref` is cheap while already handling an error,
955
+ * and keeps the success path free of it.
956
+ *
957
+ * Returns whether any of `names` is confirmed gone; `false` when the probe couldn't answer, so a
958
+ * spurious notification is never fired on an unrelated failure.
959
+ */
960
+ private async cleanupDeletedBranchMetadata(repoPath: string, names: string[]): Promise<boolean> {
961
+ try {
962
+ const result = await this.git.run(
963
+ { cwd: repoPath, errors: 'ignore' },
964
+ 'for-each-ref',
965
+ // Full refname, stripped here rather than by git. NOT `%(refname:short)`: that yields the
966
+ // shortest UNAMBIGUOUS name, so a branch with a same-named tag comes back as `heads/<name>`,
967
+ // never matches the requested name, and would read as deleted — wiping a live branch's
968
+ // metadata. `%(refname:strip=2)` also solves that, but `%(refname)` is the oldest, plainest
969
+ // field and needs no assumption about which git version gained `strip`.
970
+ '--format=%(refname)',
971
+ ...names.map(n => `refs/heads/${n}`),
972
+ );
973
+ // `errors: 'ignore'` means a failed probe RESOLVES empty, which is indistinguishable from "none
974
+ // of them survived" — and would delete every name's metadata, including user-owned keys like
975
+ // `gk-disposition` and `gk-associated-issues`. Only act on a genuine answer.
976
+ //
977
+ // BOTH conditions: `exitCode !== 0` catches a probe git ran and rejected, `status !== 'exited'`
978
+ // catches one that never produced an answer — a spawn failure or queue rejection (no exit code at
979
+ // all), or a swallowed warning (which carries git's real code, so the exit-code test alone can
980
+ // still read it as "none survived").
981
+ if (result.completion.status !== 'exited' || result.exitCode !== 0) return false;
982
+
983
+ const prefix = 'refs/heads/';
984
+ const surviving = new Set(
985
+ result.stdout
986
+ .split('\n')
987
+ .map(l => l.trim())
988
+ .filter(l => l.startsWith(prefix))
989
+ .map(l => l.substring(prefix.length)),
990
+ );
991
+
992
+ let anyDeleted = false;
993
+ for (const name of names) {
994
+ if (surviving.has(name)) continue;
995
+
996
+ anyDeleted = true;
997
+ this.cache.deleteBaseBranchName(repoPath, name);
998
+ await this.provider.config.removeGkConfigBranchSection(repoPath, name);
999
+ }
1000
+
1001
+ return anyDeleted;
1002
+ } catch {
1003
+ // Best-effort bookkeeping while already unwinding a failure — never mask the original error.
1004
+ return false;
1005
+ }
1006
+ }
1007
+
865
1008
  @debug()
866
1009
  async deleteRemoteBranch(repoPath: string, names: string | string[], remote: string): Promise<void> {
867
1010
  const branches = ensureArray(names);
@@ -1161,7 +1304,7 @@ export class BranchesGitSubProvider implements GitBranchesSubProvider {
1161
1304
  if (!data) return { status: 'clean' };
1162
1305
 
1163
1306
  const mergeConflict = parseMergeTreeConflict(data);
1164
- if (!mergeConflict.conflicts.length) return { status: 'clean' };
1307
+ if (!mergeConflict.conflicts.length) return { status: 'clean', treeOid: mergeConflict.treeOid };
1165
1308
 
1166
1309
  return {
1167
1310
  status: 'conflicts',
@@ -1172,12 +1315,137 @@ export class BranchesGitSubProvider implements GitBranchesSubProvider {
1172
1315
  files: mergeConflict.conflicts,
1173
1316
  shas: undefined, // Merge conflicts don't have a specific commit SHA
1174
1317
  },
1318
+ treeOid: mergeConflict.treeOid,
1319
+ };
1320
+ },
1321
+ { cancellation: cancellation },
1322
+ );
1323
+ }
1324
+
1325
+ /** Detects conflicts when an autostash is reapplied onto the tree an integration produced */
1326
+ @debug()
1327
+ async getPotentialStashReapplyConflicts(
1328
+ repoPath: string,
1329
+ ontoTreeOid: string,
1330
+ cancellation?: AbortSignal,
1331
+ ): Promise<ConflictDetectionResult> {
1332
+ // Requires Git v2.38+ for --write-tree with 3-arg form
1333
+ if (!(await this.git.supports('git:merge-tree:write-tree'))) {
1334
+ return createConflictDetectionError('unsupported');
1335
+ }
1336
+
1337
+ // git stash create writes commit objects but touches neither the working tree nor the stash ref, and
1338
+ // captures tracked changes only — which is exactly what autostash does
1339
+ let stashSha: string;
1340
+ try {
1341
+ const result = await this.git.run(
1342
+ { cwd: repoPath, cancellation: cancellation, errors: 'throw' },
1343
+ 'stash',
1344
+ 'create',
1345
+ );
1346
+ stashSha = result.stdout.trim();
1347
+ } catch (ex) {
1348
+ if (isCancellationError(ex)) throw ex;
1349
+
1350
+ return createConflictDetectionError('other');
1351
+ }
1352
+
1353
+ if (!stashSha) return { status: 'clean' };
1354
+
1355
+ // Key on what the simulation actually consumes — the stash's tree and its first parent (the HEAD it was
1356
+ // taken against, and so the merge base). The stash SHA itself embeds a commit timestamp, so two runs over
1357
+ // an identical tree produce different SHAs and a SHA-keyed entry could never be hit.
1358
+ let stashTree: string;
1359
+ let stashParent: string;
1360
+ try {
1361
+ const result = await this.git.run(
1362
+ { cwd: repoPath, cancellation: cancellation, errors: 'throw' },
1363
+ 'rev-parse',
1364
+ `${stashSha}^{tree}`,
1365
+ `${stashSha}^`,
1366
+ );
1367
+ [stashTree, stashParent] = result.stdout.trim().split('\n');
1368
+ } catch (ex) {
1369
+ if (isCancellationError(ex)) throw ex;
1370
+
1371
+ return createConflictDetectionError('other');
1372
+ }
1373
+
1374
+ const cacheKey: ConflictDetectionCacheKey = `reapply:${ontoTreeOid}:${stashTree}.${stashParent}`;
1375
+ return this.cache.conflictDetection.getOrCreate(
1376
+ repoPath,
1377
+ cacheKey,
1378
+ async (_cacheable, signal) => {
1379
+ // The stash commit's first parent is the HEAD it was taken against, which is the correct merge
1380
+ // base for a reapply — ours is the post-integration tree, theirs is the stash
1381
+ const step = await this.runMergeTreeStep(repoPath, `${stashSha}^`, ontoTreeOid, stashSha, signal);
1382
+ if (!step.ok) return createConflictDetectionError(step.reason);
1383
+
1384
+ if (!step.result.conflicts.length) return { status: 'clean', treeOid: step.result.treeOid };
1385
+
1386
+ return {
1387
+ status: 'conflicts',
1388
+ conflict: {
1389
+ repoPath: repoPath,
1390
+ branch: stashSha,
1391
+ target: ontoTreeOid,
1392
+ files: step.result.conflicts,
1393
+ shas: undefined,
1394
+ },
1395
+ treeOid: step.result.treeOid,
1175
1396
  };
1176
1397
  },
1177
1398
  { cancellation: cancellation },
1178
1399
  );
1179
1400
  }
1180
1401
 
1402
+ private async runMergeTreeStep(
1403
+ repoPath: string,
1404
+ base: string,
1405
+ ours: string,
1406
+ theirs: string,
1407
+ cancellation?: AbortSignal,
1408
+ ): Promise<{ ok: true; result: GitMergeConflict } | { ok: false; reason: ConflictDetectionErrorReason }> {
1409
+ const scope = getScopedLogger();
1410
+
1411
+ let data;
1412
+ try {
1413
+ const result = await this.git.run(
1414
+ { cwd: repoPath, cancellation: cancellation, errors: 'throw' },
1415
+ 'merge-tree',
1416
+ '--write-tree',
1417
+ '-z',
1418
+ '--name-only',
1419
+ '--no-messages',
1420
+ `--merge-base=${base}`,
1421
+ ours,
1422
+ theirs,
1423
+ );
1424
+ data = result.stdout;
1425
+ } catch (ex) {
1426
+ if (isCancellationError(ex)) throw ex;
1427
+
1428
+ const msg: string = ex?.toString() ?? '';
1429
+ if (GitErrors.notAValidObjectName.test(msg)) {
1430
+ scope?.error(ex, `'${ours}' or '${theirs}' not found - ensure the branches/commits exist`);
1431
+ return { ok: false, reason: 'refNotFound' };
1432
+ } else if (GitErrors.badRevision.test(msg)) {
1433
+ scope?.error(ex, `Invalid revision: ${msg.slice(msg.indexOf("'"))}`);
1434
+ return { ok: false, reason: 'refNotFound' };
1435
+ } else if (GitErrors.noMergeBase.test(msg)) {
1436
+ scope?.error(ex, `Unable to merge '${ours}' and '${theirs}' as they have no common ancestor`);
1437
+ return { ok: false, reason: 'noMergeBase' };
1438
+ } else if (ex instanceof GitError) {
1439
+ data = ex.stdout;
1440
+ } else {
1441
+ scope?.error(ex, 'Failed to execute merge-tree for conflict check');
1442
+ return { ok: false, reason: 'other' };
1443
+ }
1444
+ }
1445
+
1446
+ return { ok: true, result: parseMergeTreeConflict(data ?? '') };
1447
+ }
1448
+
1181
1449
  private async checkForPotentialConflicts(
1182
1450
  repoPath: string,
1183
1451
  commits: Iterable<{ sha: string; parent: string }>,
@@ -1214,56 +1482,16 @@ export class BranchesGitSubProvider implements GitBranchesSubProvider {
1214
1482
  for (const commit of commits) {
1215
1483
  if (cancellation?.aborted) throw new CancellationError();
1216
1484
 
1217
- let data;
1218
- try {
1219
- // Use merge-tree --write-tree with --merge-base to simulate cherry-pick:
1220
- // merge-tree --write-tree --merge-base=<commit-parent> <current-tree> <commit>
1221
- // This performs a 3-way merge where:
1222
- // - base = commit.parent (where the commit started)
1223
- // - ours = currentTreeOid (current state of the target)
1224
- // - theirs = commit.sha (what we're cherry-picking)
1225
- const result = await this.git.run(
1226
- { cwd: repoPath, cancellation: cancellation, errors: 'throw' },
1227
- 'merge-tree',
1228
- '--write-tree',
1229
- '-z',
1230
- '--name-only',
1231
- '--no-messages',
1232
- `--merge-base=${commit.parent}`,
1233
- currentTreeOid,
1234
- commit.sha,
1235
- );
1236
- data = result.stdout;
1237
- } catch (ex) {
1238
- if (isCancellationError(ex)) throw ex;
1239
-
1240
- const msg: string = ex?.toString() ?? '';
1241
- if (GitErrors.notAValidObjectName.test(msg)) {
1242
- scope?.error(
1243
- ex,
1244
- `'${targetBranch}' or '${commit.sha}' not found - ensure the branches/commits exist`,
1245
- );
1246
- return createConflictDetectionError('refNotFound');
1247
- } else if (GitErrors.badRevision.test(msg)) {
1248
- scope?.error(ex, `Invalid revision: ${msg.slice(msg.indexOf("'"))}`);
1249
- return createConflictDetectionError('refNotFound');
1250
- } else if (GitErrors.noMergeBase.test(msg)) {
1251
- scope?.error(
1252
- ex,
1253
- `Unable to merge '${commit.sha}' and '${targetBranch}' as they have no common ancestor`,
1254
- );
1255
- return createConflictDetectionError('noMergeBase');
1256
- } else if (ex instanceof GitError) {
1257
- data = ex.stdout;
1258
- } else {
1259
- scope?.error(ex, 'Failed to execute merge-tree for conflict check');
1260
- return createConflictDetectionError('other');
1261
- }
1262
- }
1263
-
1264
- if (!data) continue;
1485
+ // Use merge-tree --write-tree with --merge-base to simulate cherry-pick:
1486
+ // merge-tree --write-tree --merge-base=<commit-parent> <current-tree> <commit>
1487
+ // This performs a 3-way merge where:
1488
+ // - base = commit.parent (where the commit started)
1489
+ // - ours = currentTreeOid (current state of the target)
1490
+ // - theirs = commit.sha (what we're cherry-picking)
1491
+ const step = await this.runMergeTreeStep(repoPath, commit.parent, currentTreeOid, commit.sha, cancellation);
1492
+ if (!step.ok) return createConflictDetectionError(step.reason);
1265
1493
 
1266
- const mergeConflict = parseMergeTreeConflict(data);
1494
+ const mergeConflict = step.result;
1267
1495
 
1268
1496
  if (mergeConflict.conflicts.length) {
1269
1497
  conflictingShas.push(commit.sha);
@@ -1282,6 +1510,7 @@ export class BranchesGitSubProvider implements GitBranchesSubProvider {
1282
1510
  shas: [commit.sha],
1283
1511
  },
1284
1512
  stoppedOnFirstConflict: true,
1513
+ treeOid: mergeConflict.treeOid || undefined,
1285
1514
  };
1286
1515
  }
1287
1516
  }
@@ -1304,10 +1533,11 @@ export class BranchesGitSubProvider implements GitBranchesSubProvider {
1304
1533
  shas: conflictingShas,
1305
1534
  },
1306
1535
  stoppedOnFirstConflict: false,
1536
+ treeOid: currentTreeOid,
1307
1537
  };
1308
1538
  }
1309
1539
 
1310
- return { status: 'clean' };
1540
+ return { status: 'clean', treeOid: currentTreeOid };
1311
1541
  }
1312
1542
 
1313
1543
  @debug({ exit: true })
@@ -1323,7 +1553,7 @@ export class BranchesGitSubProvider implements GitBranchesSubProvider {
1323
1553
  return this.cache.getBaseBranchName(
1324
1554
  repoPath,
1325
1555
  ref,
1326
- async (commonPath, signal) => {
1556
+ async (commonPath, cacheable, signal) => {
1327
1557
  try {
1328
1558
  // getGkConfig has built-in fallback to regular config for backward compatibility
1329
1559
  let mergeBase = await this.provider.config.getGkConfig(commonPath, `branch.${ref}.gk-merge-base`);
@@ -1355,18 +1585,25 @@ export class BranchesGitSubProvider implements GitBranchesSubProvider {
1355
1585
  }
1356
1586
  } catch {}
1357
1587
 
1358
- const branch = await this.getBaseBranchFromReflog(
1588
+ const reflog = await this.getBaseBranchFromReflog(
1359
1589
  commonPath,
1360
1590
  ref,
1361
1591
  { upstream: true, priority: priority },
1362
1592
  signal,
1363
1593
  );
1364
- if (branch != null) {
1594
+ if (reflog.branch != null) {
1365
1595
  // Self-write: see comment on the migration path above.
1366
- void this.storeBaseBranchName(commonPath, ref, branch, {
1596
+ void this.storeBaseBranchName(commonPath, ref, reflog.branch, {
1367
1597
  skipInvalidation: ['branchOverviews', 'baseBranchName'],
1368
1598
  });
1369
- return branch;
1599
+ return reflog.branch;
1600
+ }
1601
+
1602
+ // This entry has no TTL and the `'branches'` cascade no longer clears it (a tip move can't
1603
+ // change a base), so a failed read cached as `undefined` would read as "no base branch" for
1604
+ // the rest of the session. Only a reflog that actually RAN and found nothing is an answer.
1605
+ if (reflog.failed) {
1606
+ cacheable.invalidate();
1370
1607
  }
1371
1608
 
1372
1609
  return undefined;
@@ -1375,12 +1612,16 @@ export class BranchesGitSubProvider implements GitBranchesSubProvider {
1375
1612
  );
1376
1613
  }
1377
1614
 
1615
+ /**
1616
+ * `failed` separates "the reflog ran and had no answer" from "the read never happened" — the caller
1617
+ * caches the former forever and must not cache the latter.
1618
+ */
1378
1619
  private async getBaseBranchFromReflog(
1379
1620
  repoPath: string,
1380
1621
  ref: string,
1381
1622
  options?: { upstream: true; priority?: GitCommandPriority },
1382
1623
  cancellation?: AbortSignal,
1383
- ): Promise<string | undefined> {
1624
+ ): Promise<{ branch: string | undefined; failed: boolean }> {
1384
1625
  const priority = options?.priority;
1385
1626
  const priorityOpts = priority != null ? { priority: priority } : undefined;
1386
1627
 
@@ -1393,7 +1634,7 @@ export class BranchesGitSubProvider implements GitBranchesSubProvider {
1393
1634
  );
1394
1635
 
1395
1636
  let entries = result.stdout.split('\n').filter(entry => Boolean(entry));
1396
- if (entries.length !== 1) return undefined;
1637
+ if (entries.length !== 1) return { branch: undefined, failed: false };
1397
1638
 
1398
1639
  // Check if branch created from an explicit branch
1399
1640
  let match = entries[0].match(/branch: Created from (.*)$/);
@@ -1406,11 +1647,11 @@ export class BranchesGitSubProvider implements GitBranchesSubProvider {
1406
1647
  `${name}@{u}`,
1407
1648
  priorityOpts,
1408
1649
  );
1409
- if (upstream) return upstream;
1650
+ if (upstream) return { branch: upstream, failed: false };
1410
1651
  }
1411
1652
 
1412
1653
  name = await this.provider.refs.getSymbolicReferenceName(repoPath, name, priorityOpts);
1413
- if (name) return name;
1654
+ if (name) return { branch: name, failed: false };
1414
1655
  }
1415
1656
  }
1416
1657
 
@@ -1423,7 +1664,7 @@ export class BranchesGitSubProvider implements GitBranchesSubProvider {
1423
1664
  );
1424
1665
 
1425
1666
  entries = result.stdout.split('\n').filter(entry => Boolean(entry));
1426
- if (!entries.length) return undefined;
1667
+ if (!entries.length) return { branch: undefined, failed: false };
1427
1668
 
1428
1669
  match = entries.at(-1)!.match(/checkout: moving from ([^\s]+)\s/);
1429
1670
  if (match?.length === 2) {
@@ -1434,17 +1675,21 @@ export class BranchesGitSubProvider implements GitBranchesSubProvider {
1434
1675
  `${name}@{u}`,
1435
1676
  priorityOpts,
1436
1677
  );
1437
- if (upstream) return upstream;
1678
+ if (upstream) return { branch: upstream, failed: false };
1438
1679
  }
1439
1680
 
1440
1681
  name = await this.provider.refs.getSymbolicReferenceName(repoPath, name, priorityOpts);
1441
- if (name) return name;
1682
+ if (name) return { branch: name, failed: false };
1442
1683
  }
1443
1684
  } catch (ex) {
1444
1685
  if (isCancellationError(ex)) throw ex;
1686
+
1687
+ // The reads use default error handling, so a spawn failure, queue rejection or swallowed
1688
+ // `GitWarnings` match lands here rather than producing an empty answer.
1689
+ return { branch: undefined, failed: true };
1445
1690
  }
1446
1691
 
1447
- return undefined;
1692
+ return { branch: undefined, failed: false };
1448
1693
  }
1449
1694
 
1450
1695
  @debug({ exit: true })
@@ -1506,6 +1751,13 @@ export class BranchesGitSubProvider implements GitBranchesSubProvider {
1506
1751
  const args = ['branch', '-m', oldName, newName];
1507
1752
  try {
1508
1753
  await this.git.run({ cwd: repoPath }, ...args);
1754
+ // The branch keeps its identity across a rename, so move its stored gk metadata with it —
1755
+ // otherwise the new name re-derives a base it already had, and the old name is left holding
1756
+ // metadata for a branch that no longer exists under it.
1757
+ await this.provider.config.renameGkConfigBranchSection(repoPath, oldName, newName);
1758
+ // Old name's entry is now orphaned; the new name must not inherit a predecessor's base
1759
+ this.cache.deleteBaseBranchName(repoPath, oldName);
1760
+ this.cache.deleteBaseBranchName(repoPath, newName);
1509
1761
  this.context.hooks?.cache?.onReset?.(repoPath, 'branches');
1510
1762
  this.context.hooks?.repository?.onChanged?.(repoPath, ['heads']);
1511
1763
  } catch (ex) {