@gitkraken/core-gitlens 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (223) hide show
  1. package/CHANGELOG.md +22 -1
  2. package/dist/git/cache.d.ts +21 -5
  3. package/dist/git/cache.d.ts.map +1 -1
  4. package/dist/git/cache.js +102 -6
  5. package/dist/git/cache.js.map +1 -1
  6. package/dist/git/context.d.ts +11 -2
  7. package/dist/git/context.d.ts.map +1 -1
  8. package/dist/git/context.js.map +1 -1
  9. package/dist/git/models/branch.d.ts +1 -0
  10. package/dist/git/models/branch.d.ts.map +1 -1
  11. package/dist/git/models/branch.js.map +1 -1
  12. package/dist/git/models/commit.d.ts +1 -9
  13. package/dist/git/models/commit.d.ts.map +1 -1
  14. package/dist/git/models/commit.js +4 -9
  15. package/dist/git/models/commit.js.map +1 -1
  16. package/dist/git/models/graph.d.ts +2 -4
  17. package/dist/git/models/graph.d.ts.map +1 -1
  18. package/dist/git/models/repository.d.ts +12 -3
  19. package/dist/git/models/repository.d.ts.map +1 -1
  20. package/dist/git/models/repository.js +25 -4
  21. package/dist/git/models/repository.js.map +1 -1
  22. package/dist/git/models/worktree.d.ts +1 -1
  23. package/dist/git/models/worktree.d.ts.map +1 -1
  24. package/dist/git/models/worktree.js +2 -2
  25. package/dist/git/models/worktree.js.map +1 -1
  26. package/dist/git/parsers/diffParser.d.ts +0 -1
  27. package/dist/git/parsers/diffParser.d.ts.map +1 -1
  28. package/dist/git/parsers/diffParser.js +22 -61
  29. package/dist/git/parsers/diffParser.js.map +1 -1
  30. package/dist/git/providers/branches.d.ts +3 -0
  31. package/dist/git/providers/branches.d.ts.map +1 -1
  32. package/dist/git/providers/commits.d.ts +6 -0
  33. package/dist/git/providers/commits.d.ts.map +1 -1
  34. package/dist/git/providers/config.d.ts +2 -0
  35. package/dist/git/providers/config.d.ts.map +1 -1
  36. package/dist/git/providers/diff.d.ts +2 -0
  37. package/dist/git/providers/diff.d.ts.map +1 -1
  38. package/dist/git/providers/operations.d.ts +6 -0
  39. package/dist/git/providers/operations.d.ts.map +1 -1
  40. package/dist/git/providers/revision.d.ts +1 -1
  41. package/dist/git/providers/revision.d.ts.map +1 -1
  42. package/dist/git/providers/staging.d.ts +2 -0
  43. package/dist/git/providers/staging.d.ts.map +1 -1
  44. package/dist/git/utils/sorting.d.ts +6 -0
  45. package/dist/git/utils/sorting.d.ts.map +1 -1
  46. package/dist/git/utils/sorting.js +19 -0
  47. package/dist/git/utils/sorting.js.map +1 -1
  48. package/dist/git-cli/cliGitProvider.js +1 -1
  49. package/dist/git-cli/cliGitProvider.js.map +1 -1
  50. package/dist/git-cli/exec/git.d.ts +29 -4
  51. package/dist/git-cli/exec/git.d.ts.map +1 -1
  52. package/dist/git-cli/exec/git.js +70 -9
  53. package/dist/git-cli/exec/git.js.map +1 -1
  54. package/dist/git-cli/exec/gitQueue.d.ts +12 -4
  55. package/dist/git-cli/exec/gitQueue.d.ts.map +1 -1
  56. package/dist/git-cli/exec/gitQueue.js +49 -12
  57. package/dist/git-cli/exec/gitQueue.js.map +1 -1
  58. package/dist/git-cli/parsers/logParser.d.ts +1 -0
  59. package/dist/git-cli/parsers/logParser.d.ts.map +1 -1
  60. package/dist/git-cli/parsers/logParser.js +4 -0
  61. package/dist/git-cli/parsers/logParser.js.map +1 -1
  62. package/dist/git-cli/providers/blame.js +1 -1
  63. package/dist/git-cli/providers/blame.js.map +1 -1
  64. package/dist/git-cli/providers/branches.d.ts +2 -0
  65. package/dist/git-cli/providers/branches.d.ts.map +1 -1
  66. package/dist/git-cli/providers/branches.js +176 -123
  67. package/dist/git-cli/providers/branches.js.map +1 -1
  68. package/dist/git-cli/providers/commits.d.ts +4 -0
  69. package/dist/git-cli/providers/commits.d.ts.map +1 -1
  70. package/dist/git-cli/providers/commits.js +58 -17
  71. package/dist/git-cli/providers/commits.js.map +1 -1
  72. package/dist/git-cli/providers/config.d.ts.map +1 -1
  73. package/dist/git-cli/providers/config.js +9 -9
  74. package/dist/git-cli/providers/config.js.map +1 -1
  75. package/dist/git-cli/providers/contributors.d.ts.map +1 -1
  76. package/dist/git-cli/providers/contributors.js +7 -4
  77. package/dist/git-cli/providers/contributors.js.map +1 -1
  78. package/dist/git-cli/providers/diff.d.ts +2 -0
  79. package/dist/git-cli/providers/diff.d.ts.map +1 -1
  80. package/dist/git-cli/providers/diff.js +89 -19
  81. package/dist/git-cli/providers/diff.js.map +1 -1
  82. package/dist/git-cli/providers/graph.d.ts.map +1 -1
  83. package/dist/git-cli/providers/graph.js +43 -27
  84. package/dist/git-cli/providers/graph.js.map +1 -1
  85. package/dist/git-cli/providers/operations.d.ts +17 -0
  86. package/dist/git-cli/providers/operations.d.ts.map +1 -1
  87. package/dist/git-cli/providers/operations.js +47 -14
  88. package/dist/git-cli/providers/operations.js.map +1 -1
  89. package/dist/git-cli/providers/patch.d.ts.map +1 -1
  90. package/dist/git-cli/providers/patch.js +15 -30
  91. package/dist/git-cli/providers/patch.js.map +1 -1
  92. package/dist/git-cli/providers/pausedOperations.js +6 -6
  93. package/dist/git-cli/providers/pausedOperations.js.map +1 -1
  94. package/dist/git-cli/providers/refs.d.ts.map +1 -1
  95. package/dist/git-cli/providers/refs.js +26 -7
  96. package/dist/git-cli/providers/refs.js.map +1 -1
  97. package/dist/git-cli/providers/remotes.js +4 -4
  98. package/dist/git-cli/providers/remotes.js.map +1 -1
  99. package/dist/git-cli/providers/revision.d.ts +1 -1
  100. package/dist/git-cli/providers/revision.d.ts.map +1 -1
  101. package/dist/git-cli/providers/revision.js +15 -11
  102. package/dist/git-cli/providers/revision.js.map +1 -1
  103. package/dist/git-cli/providers/staging.d.ts +2 -0
  104. package/dist/git-cli/providers/staging.d.ts.map +1 -1
  105. package/dist/git-cli/providers/staging.js +20 -8
  106. package/dist/git-cli/providers/staging.js.map +1 -1
  107. package/dist/git-cli/providers/stash.d.ts +0 -8
  108. package/dist/git-cli/providers/stash.d.ts.map +1 -1
  109. package/dist/git-cli/providers/stash.js +63 -119
  110. package/dist/git-cli/providers/stash.js.map +1 -1
  111. package/dist/git-cli/providers/status.js +6 -6
  112. package/dist/git-cli/providers/status.js.map +1 -1
  113. package/dist/git-cli/providers/tags.js +4 -4
  114. package/dist/git-cli/providers/tags.js.map +1 -1
  115. package/dist/git-cli/providers/worktrees.js +3 -3
  116. package/dist/git-cli/providers/worktrees.js.map +1 -1
  117. package/dist/plus/ai/constants.d.ts +2 -1
  118. package/dist/plus/ai/constants.d.ts.map +1 -1
  119. package/dist/plus/ai/constants.js +7 -0
  120. package/dist/plus/ai/constants.js.map +1 -1
  121. package/dist/plus/ai/models/model.d.ts +1 -1
  122. package/dist/plus/ai/models/model.d.ts.map +1 -1
  123. package/dist/plus/ai/models/model.js.map +1 -1
  124. package/dist/plus/ai/models/promptTemplates.d.ts +19 -2
  125. package/dist/plus/ai/models/promptTemplates.d.ts.map +1 -1
  126. package/dist/plus/ai/models/results.d.ts +28 -0
  127. package/dist/plus/ai/models/results.d.ts.map +1 -1
  128. package/dist/plus/ai/prompts.d.ts +3 -0
  129. package/dist/plus/ai/prompts.d.ts.map +1 -1
  130. package/dist/plus/ai/prompts.js +126 -0
  131. package/dist/plus/ai/prompts.js.map +1 -1
  132. package/dist/plus/ai/utils/ai.utils.d.ts.map +1 -1
  133. package/dist/plus/ai/utils/ai.utils.js +2 -0
  134. package/dist/plus/ai/utils/ai.utils.js.map +1 -1
  135. package/dist/plus/ai/utils/results.utils.d.ts +3 -0
  136. package/dist/plus/ai/utils/results.utils.d.ts.map +1 -1
  137. package/dist/plus/ai/utils/results.utils.js +75 -0
  138. package/dist/plus/ai/utils/results.utils.js.map +1 -1
  139. package/dist/plus/ai/utils/truncation.utils.d.ts +1 -1
  140. package/dist/plus/ai/utils/truncation.utils.d.ts.map +1 -1
  141. package/dist/plus/ai/utils/truncation.utils.js.map +1 -1
  142. package/dist/plus/git-github/providers/github/branches.d.ts.map +1 -1
  143. package/dist/plus/git-github/providers/github/branches.js +16 -1
  144. package/dist/plus/git-github/providers/github/branches.js.map +1 -1
  145. package/dist/plus/git-github/providers/github/diff.d.ts +2 -0
  146. package/dist/plus/git-github/providers/github/diff.d.ts.map +1 -1
  147. package/dist/plus/git-github/providers/github/diff.js.map +1 -1
  148. package/dist/utils/decorators/log.js +1 -1
  149. package/dist/utils/decorators/log.js.map +1 -1
  150. package/dist/utils/decorators/sequentialize.d.ts.map +1 -1
  151. package/dist/utils/decorators/sequentialize.js +4 -2
  152. package/dist/utils/decorators/sequentialize.js.map +1 -1
  153. package/dist/utils/env/node/base64.js.map +1 -1
  154. package/dist/utils/env/node/exec.js.map +1 -1
  155. package/dist/utils/formatter.d.ts.map +1 -1
  156. package/dist/utils/formatter.js +4 -10
  157. package/dist/utils/formatter.js.map +1 -1
  158. package/dist/utils/iterable.js.map +1 -1
  159. package/dist/utils/lruMap.d.ts +40 -0
  160. package/dist/utils/lruMap.d.ts.map +1 -0
  161. package/dist/utils/lruMap.js +81 -0
  162. package/dist/utils/lruMap.js.map +1 -0
  163. package/dist/utils/version.d.ts +14 -0
  164. package/dist/utils/version.d.ts.map +1 -1
  165. package/dist/utils/version.js +48 -0
  166. package/dist/utils/version.js.map +1 -1
  167. package/package.json +1 -1
  168. package/src/git/cache.ts +179 -13
  169. package/src/git/context.ts +11 -2
  170. package/src/git/models/branch.ts +1 -0
  171. package/src/git/models/commit.ts +1 -17
  172. package/src/git/models/graph.ts +2 -4
  173. package/src/git/models/repository.ts +26 -6
  174. package/src/git/models/worktree.ts +2 -2
  175. package/src/git/parsers/diffParser.ts +7 -44
  176. package/src/git/providers/branches.ts +3 -0
  177. package/src/git/providers/commits.ts +7 -0
  178. package/src/git/providers/config.ts +2 -0
  179. package/src/git/providers/diff.ts +8 -2
  180. package/src/git/providers/operations.ts +10 -3
  181. package/src/git/providers/revision.ts +5 -1
  182. package/src/git/providers/staging.ts +2 -0
  183. package/src/git/utils/sorting.ts +21 -0
  184. package/src/git-cli/cliGitProvider.ts +1 -1
  185. package/src/git-cli/exec/git.ts +70 -18
  186. package/src/git-cli/exec/gitQueue.ts +57 -13
  187. package/src/git-cli/parsers/logParser.ts +5 -1
  188. package/src/git-cli/providers/blame.ts +1 -1
  189. package/src/git-cli/providers/branches.ts +207 -133
  190. package/src/git-cli/providers/commits.ts +71 -18
  191. package/src/git-cli/providers/config.ts +9 -14
  192. package/src/git-cli/providers/contributors.ts +7 -12
  193. package/src/git-cli/providers/diff.ts +100 -19
  194. package/src/git-cli/providers/graph.ts +47 -27
  195. package/src/git-cli/providers/operations.ts +77 -18
  196. package/src/git-cli/providers/patch.ts +15 -37
  197. package/src/git-cli/providers/pausedOperations.ts +6 -6
  198. package/src/git-cli/providers/refs.ts +27 -8
  199. package/src/git-cli/providers/remotes.ts +4 -4
  200. package/src/git-cli/providers/revision.ts +20 -13
  201. package/src/git-cli/providers/staging.ts +18 -8
  202. package/src/git-cli/providers/stash.ts +67 -134
  203. package/src/git-cli/providers/status.ts +6 -6
  204. package/src/git-cli/providers/tags.ts +4 -4
  205. package/src/git-cli/providers/worktrees.ts +3 -3
  206. package/src/plus/ai/constants.ts +8 -0
  207. package/src/plus/ai/models/model.ts +1 -0
  208. package/src/plus/ai/models/promptTemplates.ts +29 -0
  209. package/src/plus/ai/models/results.ts +30 -0
  210. package/src/plus/ai/prompts.ts +129 -0
  211. package/src/plus/ai/utils/ai.utils.ts +2 -0
  212. package/src/plus/ai/utils/results.utils.ts +99 -0
  213. package/src/plus/ai/utils/truncation.utils.ts +3 -1
  214. package/src/plus/git-github/providers/github/branches.ts +17 -5
  215. package/src/plus/git-github/providers/github/diff.ts +8 -2
  216. package/src/utils/decorators/log.ts +1 -1
  217. package/src/utils/decorators/sequentialize.ts +8 -6
  218. package/src/utils/env/node/base64.ts +1 -1
  219. package/src/utils/env/node/exec.ts +2 -2
  220. package/src/utils/formatter.ts +4 -11
  221. package/src/utils/iterable.ts +1 -1
  222. package/src/utils/lruMap.ts +87 -0
  223. package/src/utils/version.ts +48 -0
package/src/git/cache.ts CHANGED
@@ -8,10 +8,12 @@ import type { PromiseOrValue } from '../utils/promise.js';
8
8
  import type { CacheController } from '../utils/promiseCache.js';
9
9
  import { PromiseCache, PromiseMap, RepoPromiseCacheMap } from '../utils/promiseCache.js';
10
10
  import type { Uri } from '../utils/uri.js';
11
+ import { getBranchId } from './utils/branch.utils.js';
12
+ import { createReference } from './utils/reference.utils.js';
11
13
  import { getCommonRepositoryPath, getRepositoryOrWorktreePath } from './utils/repository.utils.js';
12
14
  import type { GitResult } from './exec.types.js';
13
15
  import type { ProgressiveGitBlame } from './models/blame.js';
14
- import type { GitBranch } from './models/branch.js';
16
+ import type { BranchMetadata, GitBranch } from './models/branch.js';
15
17
  import type { GitStashCommit } from './models/commit.js';
16
18
  import type { GitContributor, GitContributorsStats } from './models/contributor.js';
17
19
  import type { ParsedGitDiffHunks } from './models/diff.js';
@@ -26,6 +28,7 @@ import type { GitStash } from './models/stash.js';
26
28
  import type { GitTag } from './models/tag.js';
27
29
  import type { GitUser } from './models/user.js';
28
30
  import type { GitWorktree } from './models/worktree.js';
31
+ import type { BranchContributionsOverview, GitBranchMergedStatus } from './providers/branches.js';
29
32
  import type { GitCommitReachability, LeftRightCommitCountResult } from './providers/commits.js';
30
33
  import type { GitContributorsResult } from './providers/contributors.js';
31
34
  import type { ResolvedRevision } from './providers/revision.js';
@@ -49,6 +52,7 @@ export type CachedGitTypes =
49
52
  | 'contributors'
50
53
  | 'gitignore'
51
54
  | 'gkConfig'
55
+ | 'lastFetched'
52
56
  | 'providers'
53
57
  | 'remotes'
54
58
  | 'stashes'
@@ -59,6 +63,12 @@ export type CachedGitTypes =
59
63
 
60
64
  export type ConflictDetectionCacheKey = `apply:${string}:${string}:${string}` | `merge:${string}:${string}`;
61
65
 
66
+ /**
67
+ * gkConfig keys that participate in the `branchOverviews` cache's mergeTarget/mergeBase lineage.
68
+ * Capture group 1 is the ref name (which may itself contain `.`/`/`).
69
+ */
70
+ const branchOverviewGkConfigKeyPattern = /^branch\.(.+)\.(?:gk-merge-(?:base|target(?:-user)?)|gk-target-base)$/;
71
+
62
72
  /** Per-worktree caches — cleared by repoPath directly */
63
73
  interface Caches {
64
74
  bestRemotes: PromiseMap<RepoPath, GitRemote<RemoteProvider>[]> | undefined;
@@ -83,6 +93,10 @@ interface Caches {
83
93
 
84
94
  /** Shared caches — cleared via commonPath + all worktree paths */
85
95
  interface SharedCaches {
96
+ baseBranchName: RepoPromiseCacheMap<string, string | undefined> | undefined;
97
+ branchMergedStatus: RepoPromiseCacheMap<string, GitBranchMergedStatus> | undefined;
98
+ branchMetadataMap: PromiseMap<RepoPath, Map<string, BranchMetadata>> | undefined;
99
+ branchOverviews: RepoPromiseCacheMap<string, BranchContributionsOverview | undefined> | undefined;
86
100
  branches: PromiseMap<RepoPath, PagedResult<GitBranch>> | undefined;
87
101
  configKeys: RepoPromiseCacheMap<string, string | undefined> | undefined;
88
102
  configPatterns: RepoPromiseCacheMap<string, Map<string, string>> | undefined;
@@ -97,7 +111,7 @@ interface SharedCaches {
97
111
  logShas: RepoPromiseCacheMap<string, string[]> | undefined;
98
112
  remotes: PromiseMap<RepoPath, GitRemote[]> | undefined;
99
113
  sharedBranches: PromiseMap<RepoPath, PagedResult<GitBranch>> | undefined;
100
- stashes: PromiseMap<RepoPath, GitStash> | undefined;
114
+ stashes: RepoPromiseCacheMap<string, GitStash> | undefined;
101
115
  tags: PromiseMap<RepoPath, PagedResult<GitTag>> | undefined;
102
116
  worktrees: PromiseMap<RepoPath, GitWorktree[]> | undefined;
103
117
  }
@@ -107,6 +121,10 @@ type AllCaches = Caches & SharedCaches;
107
121
  /** Compile-time enforced: adding a key to SharedCaches without listing it here is a type error */
108
122
  const sharedCacheKeys: ReadonlySet<keyof AllCaches> = new Set(
109
123
  exhaustiveArray<keyof SharedCaches>()([
124
+ 'baseBranchName',
125
+ 'branchMergedStatus',
126
+ 'branchMetadataMap',
127
+ 'branchOverviews',
110
128
  'branches',
111
129
  'configKeys',
112
130
  'configPatterns',
@@ -129,9 +147,13 @@ const sharedCacheKeys: ReadonlySet<keyof AllCaches> = new Set(
129
147
 
130
148
  function createEmptyCaches(): AllCaches {
131
149
  return {
150
+ baseBranchName: undefined,
132
151
  bestRemotes: undefined,
133
152
  blame: undefined,
134
153
  branch: undefined,
154
+ branchMergedStatus: undefined,
155
+ branchMetadataMap: undefined,
156
+ branchOverviews: undefined,
135
157
  branches: undefined,
136
158
  fileExistence: undefined,
137
159
  ignoreRevsFile: undefined,
@@ -206,10 +228,33 @@ export class Cache implements Disposable {
206
228
  }));
207
229
  }
208
230
 
231
+ private get baseBranchName(): RepoPromiseCacheMap<string, string | undefined> {
232
+ return (this._caches.baseBranchName ??= new RepoPromiseCacheMap<string, string | undefined>());
233
+ }
234
+
209
235
  get branch(): PromiseMap<RepoPath, GitBranch | undefined> {
210
236
  return (this._caches.branch ??= new PromiseMap<RepoPath, GitBranch | undefined>());
211
237
  }
212
238
 
239
+ get branchMergedStatus(): RepoPromiseCacheMap<string, GitBranchMergedStatus> {
240
+ return (this._caches.branchMergedStatus ??= new RepoPromiseCacheMap<string, GitBranchMergedStatus>({
241
+ createTTL: 1000 * 60 * 5, // 5 minutes
242
+ }));
243
+ }
244
+
245
+ private get branchMetadataMap(): PromiseMap<RepoPath, Map<string, BranchMetadata>> {
246
+ return (this._caches.branchMetadataMap ??= new PromiseMap<RepoPath, Map<string, BranchMetadata>>());
247
+ }
248
+
249
+ private get branchOverviews(): RepoPromiseCacheMap<string, BranchContributionsOverview | undefined> {
250
+ return (this._caches.branchOverviews ??= new RepoPromiseCacheMap<
251
+ string,
252
+ BranchContributionsOverview | undefined
253
+ >(
254
+ { accessTTL: 1000 * 60 * 60 }, // 60 minutes
255
+ ));
256
+ }
257
+
213
258
  get branches(): PromiseMap<RepoPath, PagedResult<GitBranch>> {
214
259
  return (this._caches.branches ??= new PromiseMap<RepoPath, PagedResult<GitBranch>>());
215
260
  }
@@ -364,8 +409,10 @@ export class Cache implements Disposable {
364
409
  return (this._caches.remotes ??= new PromiseMap<RepoPath, GitRemote[]>());
365
410
  }
366
411
 
367
- get stashes(): PromiseMap<RepoPath, GitStash> {
368
- return (this._caches.stashes ??= new PromiseMap<RepoPath, GitStash>());
412
+ get stashes(): RepoPromiseCacheMap<string, GitStash> {
413
+ return (this._caches.stashes ??= new RepoPromiseCacheMap<string, GitStash>({
414
+ accessTTL: 1000 * 60 * 60, // 60 minutes
415
+ }));
369
416
  }
370
417
 
371
418
  get tags(): PromiseMap<RepoPath, PagedResult<GitTag>> {
@@ -424,9 +471,14 @@ export class Cache implements Disposable {
424
471
  keysToClear.add('fileLog');
425
472
  }
426
473
 
427
- // Per-worktree branch caches (current branch / HEAD sensitive)
474
+ // Branch caches affected by HEAD/heads changes — `branchMergedStatus` is shared
475
+ // (the answer is invariant across worktrees of the same common repo) but is invalidated
476
+ // here because branch-tip movement in any worktree can change the merge status; the
477
+ // shared-clear logic below routes shared keys via commonPath.
428
478
  if (types.includes('branch') || types.includes('branches')) {
429
479
  keysToClear.add('branch');
480
+ keysToClear.add('branchMergedStatus');
481
+ keysToClear.add('branchOverviews');
430
482
  keysToClear.add('conflictDetection');
431
483
  keysToClear.add('currentBranchReference');
432
484
  keysToClear.add('leftRightCommitCount');
@@ -436,6 +488,8 @@ export class Cache implements Disposable {
436
488
 
437
489
  // Shared branch caches (branch list, metadata)
438
490
  if (types.includes('branches')) {
491
+ keysToClear.add('baseBranchName');
492
+ keysToClear.add('branchMetadataMap');
439
493
  keysToClear.add('branches');
440
494
  keysToClear.add('sharedBranches');
441
495
  keysToClear.add('defaultBranchName');
@@ -452,6 +506,7 @@ export class Cache implements Disposable {
452
506
  }
453
507
 
454
508
  if (types.includes('contributors')) {
509
+ keysToClear.add('branchOverviews');
455
510
  keysToClear.add('contributors');
456
511
  keysToClear.add('contributorsLite');
457
512
  keysToClear.add('contributorsStats');
@@ -464,6 +519,16 @@ export class Cache implements Disposable {
464
519
  if (types.includes('gkConfig')) {
465
520
  keysToClear.add('gkConfigKeys');
466
521
  keysToClear.add('gkConfigPatterns');
522
+ // Derived from `branch.<ref>.gk-merge-base`/`vscode-merge-base`; clear so external
523
+ // gkConfig mutations don't leave stale base-branch resolutions cached.
524
+ keysToClear.add('baseBranchName');
525
+ // Cached overviews use Tier 1's stored merge target as their key-stability anchor;
526
+ // stored-target changes must invalidate or readers see stale `mergeTarget`/stats.
527
+ keysToClear.add('branchOverviews');
528
+ }
529
+
530
+ if (types.includes('lastFetched')) {
531
+ keysToClear.add('lastFetched');
467
532
  }
468
533
 
469
534
  if (types.includes('providers')) {
@@ -700,6 +765,10 @@ export class Cache implements Disposable {
700
765
  types.add('gkConfig');
701
766
  }
702
767
 
768
+ if (hasAny('lastFetched')) {
769
+ types.add('lastFetched');
770
+ }
771
+
703
772
  if (hasAny('remoteProviders')) {
704
773
  types.add('providers');
705
774
  }
@@ -859,6 +928,42 @@ export class Cache implements Disposable {
859
928
  const cacheKey = this.getCommonPath(repoPath);
860
929
  this._caches.gkConfigKeys?.delete(cacheKey, key);
861
930
  this._caches.gkConfigPatterns?.delete(cacheKey);
931
+
932
+ // When the change affects a branch's merge-target/base lineage, invalidate that branch's
933
+ // cached overview so subsequent reads pick up the new stored value rather than stale data.
934
+ // `getBranchContributionsOverview` keys `branchOverviews` by ref, with Tier 1's stored
935
+ // target as the key-stability anchor — so any write to those keys must invalidate.
936
+ const refMatch = key.match(branchOverviewGkConfigKeyPattern);
937
+ if (refMatch != null) {
938
+ this._caches.branchOverviews?.delete(cacheKey, refMatch[1]);
939
+ }
940
+ }
941
+
942
+ async getBranchOverview(
943
+ repoPath: string,
944
+ cacheKey: string,
945
+ factory: (
946
+ commonPath: string,
947
+ cacheable: CacheController,
948
+ cancellation?: AbortSignal,
949
+ ) => PromiseOrValue<BranchContributionsOverview | undefined>,
950
+ options?: { accessTTL?: number; cancellation?: AbortSignal },
951
+ ): Promise<BranchContributionsOverview | undefined> {
952
+ return this.getSharedOrCreateWithKey(
953
+ this.branchOverviews,
954
+ repoPath,
955
+ cacheKey,
956
+ factory,
957
+ (data, newRepoPath) =>
958
+ data == null
959
+ ? data
960
+ : {
961
+ ...data,
962
+ repoPath: newRepoPath,
963
+ contributors: data.contributors.map(c => c.withRepoPath(newRepoPath)),
964
+ },
965
+ options,
966
+ );
862
967
  }
863
968
 
864
969
  async getContributors(
@@ -907,12 +1012,66 @@ export class Cache implements Disposable {
907
1012
  getContributorsStats(
908
1013
  repoPath: string,
909
1014
  cacheKey: string,
910
- factory: (commonPath: string) => PromiseOrValue<GitContributorsStats | undefined>,
911
- options?: { accessTTL?: number },
1015
+ factory: (commonPath: string, cancellation?: AbortSignal) => PromiseOrValue<GitContributorsStats | undefined>,
1016
+ options?: { accessTTL?: number; cancellation?: AbortSignal },
912
1017
  ): Promise<GitContributorsStats | undefined> {
913
1018
  return this.getSharedSimpleWithKey(this.contributorsStats, repoPath, cacheKey, factory, options);
914
1019
  }
915
1020
 
1021
+ getBaseBranchName(
1022
+ repoPath: string,
1023
+ ref: string,
1024
+ factory: (commonPath: string, cancellation?: AbortSignal) => PromiseOrValue<string | undefined>,
1025
+ cancellation?: AbortSignal,
1026
+ ): Promise<string | undefined> {
1027
+ return this.getSharedSimpleWithKey(this.baseBranchName, repoPath, ref, factory, {
1028
+ cancellation: cancellation,
1029
+ });
1030
+ }
1031
+
1032
+ getBranchMergedStatus(
1033
+ repoPath: string,
1034
+ cacheKey: string,
1035
+ factory: (
1036
+ commonPath: string,
1037
+ cacheable: CacheController,
1038
+ cancellation?: AbortSignal,
1039
+ ) => PromiseOrValue<GitBranchMergedStatus>,
1040
+ cancellation?: AbortSignal,
1041
+ ): Promise<GitBranchMergedStatus> {
1042
+ return this.getSharedOrCreateWithKey(
1043
+ this.branchMergedStatus,
1044
+ repoPath,
1045
+ cacheKey,
1046
+ factory,
1047
+ (data, newRepoPath) => {
1048
+ if (!data.merged) return data;
1049
+ if (data.localBranchOnly == null) return data;
1050
+
1051
+ const lbo = data.localBranchOnly;
1052
+ return {
1053
+ ...data,
1054
+ localBranchOnly: createReference(lbo.ref, newRepoPath, {
1055
+ id: getBranchId(newRepoPath, lbo.remote, lbo.name),
1056
+ refType: 'branch',
1057
+ name: lbo.name,
1058
+ remote: lbo.remote,
1059
+ upstream: lbo.upstream,
1060
+ sha: lbo.sha,
1061
+ }),
1062
+ };
1063
+ },
1064
+ { cancellation: cancellation },
1065
+ );
1066
+ }
1067
+
1068
+ getBranchMetadataMap(
1069
+ repoPath: string,
1070
+ factory: (commonPath: string) => PromiseOrValue<Map<string, BranchMetadata>>,
1071
+ ): Promise<Map<string, BranchMetadata>> {
1072
+ return this.getSharedSimple(this.branchMetadataMap, repoPath, factory);
1073
+ }
1074
+
916
1075
  getDefaultBranchName(
917
1076
  repoPath: string,
918
1077
  remote: string,
@@ -955,16 +1114,18 @@ export class Cache implements Disposable {
955
1114
 
956
1115
  async getStash(
957
1116
  repoPath: string,
1117
+ cacheKey: string,
958
1118
  factory: (
959
1119
  commonPath: string,
960
1120
  cacheable: CacheController,
961
1121
  cancellation?: AbortSignal,
962
1122
  ) => PromiseOrValue<GitStash>,
963
- cancellation?: AbortSignal,
1123
+ options?: { accessTTL?: number; cancellation?: AbortSignal },
964
1124
  ): Promise<GitStash> {
965
- return this.getSharedOrCreate(
1125
+ return this.getSharedOrCreateWithKey(
966
1126
  this.stashes,
967
1127
  repoPath,
1128
+ cacheKey,
968
1129
  factory,
969
1130
  (data, newRepoPath) => ({
970
1131
  repoPath: newRepoPath,
@@ -975,7 +1136,7 @@ export class Cache implements Disposable {
975
1136
  ]),
976
1137
  ),
977
1138
  }),
978
- cancellation,
1139
+ options,
979
1140
  );
980
1141
  }
981
1142
 
@@ -1142,11 +1303,16 @@ export class Cache implements Disposable {
1142
1303
  cache: RepoPromiseCacheMap<string, T>,
1143
1304
  repoPath: string,
1144
1305
  cacheKey: string,
1145
- factory: (commonPath: string) => PromiseOrValue<T>,
1146
- options?: { accessTTL?: number },
1306
+ factory: (commonPath: string, cancellation?: AbortSignal) => PromiseOrValue<T>,
1307
+ options?: { accessTTL?: number; cancellation?: AbortSignal },
1147
1308
  ): Promise<T> {
1148
1309
  const commonPath = this.getCommonPath(repoPath);
1149
1310
 
1150
- return cache.getOrCreate(commonPath, cacheKey, () => Promise.resolve(factory(commonPath)), options);
1311
+ return cache.getOrCreate(
1312
+ commonPath,
1313
+ cacheKey,
1314
+ (_cacheable, signal) => Promise.resolve(factory(commonPath, signal)),
1315
+ options,
1316
+ );
1151
1317
  }
1152
1318
  }
@@ -113,9 +113,18 @@ export interface GitServiceHooks {
113
113
  };
114
114
  /** Commit lifecycle hooks */
115
115
  readonly commits?: {
116
- /** Called when a commit is signed successfully */
116
+ /**
117
+ * Called when a commit is signed successfully.
118
+ *
119
+ * Note: currently fired only from explicit-sign paths in the patch provider
120
+ * (where signing is requested via `-S` and confirmed up front). Operations
121
+ * that sign implicitly via `commit.gpgsign=true` (`commit`, `merge`, `pull`,
122
+ * `rebase`, `revert`, `cherryPick`) do not fire this hook because the
123
+ * library cannot cheaply confirm that signing actually occurred without an
124
+ * extra `git config`/`log --show-signature` call.
125
+ */
117
126
  onSigned?(format: SigningFormat, source?: unknown): void;
118
- /** Called when commit signing fails */
127
+ /** Called when commit signing fails — fired from all signing-capable paths. */
119
128
  onSigningFailed?(reason: SigningErrorReason, format: SigningFormat, source?: unknown): void;
120
129
  };
121
130
  /** Git operation hooks */
@@ -21,6 +21,7 @@ export type BranchDisposition = 'starred' | 'archived';
21
21
  export interface BranchMetadata {
22
22
  lastAccessedAt?: string;
23
23
  lastModifiedAt?: string;
24
+ agentLastActivityAt?: string;
24
25
  disposition?: BranchDisposition;
25
26
  }
26
27
 
@@ -177,10 +177,7 @@ export class GitCommit implements GitRevisionReference {
177
177
 
178
178
  const stashFiles = getSettledValue(stashFilesResult);
179
179
  if (stashFiles?.length) {
180
- commit.applyFileset({
181
- files: stashFiles as readonly GitFileChange[],
182
- filtered: commit.fileset?.filtered,
183
- });
180
+ commit.applyFileset({ files: stashFiles, filtered: commit.fileset?.filtered });
184
181
  }
185
182
  enrichCommit(commit, { stashUntrackedFilesLoaded: true });
186
183
  } else {
@@ -218,7 +215,6 @@ export class GitCommit implements GitRevisionReference {
218
215
  readonly stashNumber: string | undefined;
219
216
  readonly stashOnRef: string | undefined;
220
217
  readonly tips: string[] | undefined;
221
- readonly parentTimestamps?: GitStashParentInfo[] | undefined;
222
218
 
223
219
  constructor(
224
220
  public readonly repoPath: string,
@@ -234,13 +230,11 @@ export class GitCommit implements GitRevisionReference {
234
230
  tips?: string[],
235
231
  stashName?: string | undefined,
236
232
  stashOnRef?: string | undefined,
237
- parentTimestamps?: GitStashParentInfo[] | undefined,
238
233
  shaLength: number = getAbbreviatedShaLength(),
239
234
  ) {
240
235
  this.ref = sha;
241
236
  this.shortSha = sha.substring(0, shaLength);
242
237
  this.tips = tips;
243
- this.parentTimestamps = parentTimestamps;
244
238
 
245
239
  if (stashName) {
246
240
  this.refType = 'stash';
@@ -457,7 +451,6 @@ export class GitCommit implements GitRevisionReference {
457
451
  stats?: GitCommitStats | null;
458
452
  stashUntrackedFilesLoaded?: boolean;
459
453
  resolvedPreviousSha?: string;
460
- parentTimestamps?: GitStashParentInfo[] | null;
461
454
  }): T {
462
455
  const commit = new GitCommit(
463
456
  this.repoPath,
@@ -473,7 +466,6 @@ export class GitCommit implements GitRevisionReference {
473
466
  this.tips,
474
467
  this.stashName,
475
468
  this.stashOnRef,
476
- this.getChangedValue(changes.parentTimestamps, this.parentTimestamps),
477
469
  this.shortSha.length,
478
470
  );
479
471
  commit._etagWorkingTree = this._etagWorkingTree;
@@ -522,7 +514,6 @@ export class GitCommit implements GitRevisionReference {
522
514
  this.tips,
523
515
  this.stashName,
524
516
  this.stashOnRef,
525
- this.parentTimestamps,
526
517
  );
527
518
  commit._etagWorkingTree = this._etagWorkingTree;
528
519
  return commit as T;
@@ -672,17 +663,10 @@ export interface GitCommitStats<Files extends number | GitDiffFileStats = number
672
663
  readonly deletions: number;
673
664
  }
674
665
 
675
- export interface GitStashParentInfo {
676
- readonly sha: string;
677
- readonly authorDate?: number;
678
- readonly committerDate?: number;
679
- }
680
-
681
666
  export interface GitStashCommit extends GitCommit {
682
667
  readonly refType: GitStashReference['refType'];
683
668
  readonly stashName: string;
684
669
  readonly stashNumber: string;
685
- readonly parentTimestamps?: GitStashParentInfo[];
686
670
  }
687
671
 
688
672
  export type GitCommitWithFullDetails = GitCommit & { message: string; fileset: GitCommitFileset };
@@ -1,3 +1,4 @@
1
+ import type { GitCommitReachability } from '../providers/commits.js';
1
2
  import type { GitBranch } from './branch.js';
2
3
  import type { GitStashCommit } from './commit.js';
3
4
  import type { GitRemote } from './remote.js';
@@ -77,7 +78,7 @@ export interface GitGraphRow {
77
78
  tags?: GitGraphRowTag[];
78
79
  contexts?: GitGraphRowContexts;
79
80
  stats?: GitGraphRowStats;
80
- reachableFromBranches?: string[];
81
+ reachability?: GitCommitReachability;
81
82
  isCurrentUser?: boolean;
82
83
  }
83
84
 
@@ -97,8 +98,6 @@ export interface GitGraph {
97
98
 
98
99
  /** SHAs reachable from HEAD (for enrichment: marking commits on the current branch) */
99
100
  readonly reachableFromHEAD?: ReadonlySet<string>;
100
- /** Map of SHA → set of branch names reachable from that SHA (for enrichment: branch context) */
101
- readonly reachableFromBranches?: ReadonlyMap<string, ReadonlySet<string>>;
102
101
 
103
102
  /** The rows for the set of commits requested */
104
103
  readonly rows: GitGraphRow[];
@@ -179,6 +178,5 @@ export interface GraphContext {
179
178
  readonly branchIdOfMainWorktree: string | undefined;
180
179
  readonly stashes: ReadonlyMap<string, GitStashCommit> | undefined;
181
180
  readonly reachableFromHEAD: ReadonlySet<string>;
182
- readonly reachableFromBranches: ReadonlyMap<string, ReadonlySet<string>>;
183
181
  readonly avatars: Map<string, string>;
184
182
  }
@@ -48,7 +48,8 @@ export type RepositoryChange =
48
48
  | 'remoteProviders'
49
49
  | 'starred'
50
50
  | 'opened'
51
- | 'gkConfig';
51
+ | 'gkConfig'
52
+ | 'lastFetched';
52
53
 
53
54
  export const repositoryChanges = exhaustiveArray<RepositoryChange>()([
54
55
  'unknown',
@@ -71,6 +72,7 @@ export const repositoryChanges = exhaustiveArray<RepositoryChange>()([
71
72
  'starred',
72
73
  'opened',
73
74
  'gkConfig',
75
+ 'lastFetched',
74
76
  ]);
75
77
 
76
78
  export interface RepositoryInit {
@@ -123,6 +125,7 @@ export class Repository {
123
125
  protected readonly _watchService: RepositoryWatchService;
124
126
 
125
127
  private _pendingWorkingTreeChange?: RepositoryWorkingTreeChangeEvent;
128
+ private _pendingWorkingTreeFlush = false;
126
129
  private _repoChangeListener: UnifiedDisposable | undefined;
127
130
 
128
131
  constructor(init: RepositoryInit) {
@@ -356,6 +359,9 @@ export class Repository {
356
359
  /** Called when the watch service notifies of FETCH_HEAD changes. */
357
360
  protected onFetchHeadChanged(): void {
358
361
  this._lastFetched = undefined;
362
+ // Force-fire so the event arrives immediately (no debounce); downstream consumers must
363
+ // invalidate any cached fetch timestamp before reading a fresh value.
364
+ this.fireChange('lastFetched', true);
359
365
  }
360
366
 
361
367
  /** Called when .gitignore changes in the working tree. */
@@ -374,7 +380,15 @@ export class Repository {
374
380
  this._onDidChange.fire(extEvent);
375
381
  }
376
382
 
377
- /** Called by the session when debounced working tree changes arrive */
383
+ /**
384
+ * Called by every {@link watchWorkingTree} subscription when its debounced batch arrives. Each
385
+ * call to {@link watchWorkingTree} adds a separate bridge listener on the underlying watch
386
+ * session, so a single fs change can invoke this method multiple times in the same microtask —
387
+ * once per active subscription. We accumulate paths into a single pending event and defer the
388
+ * actual fire to a microtask so back-to-back synchronous bridge calls collapse into ONE
389
+ * `_onDidChangeWorkingTree` emission. Without this coalescing, every WIP-dependent listener
390
+ * (graph, file history, compare branch, etc.) would fire N times for N active subscriptions.
391
+ */
378
392
  protected onWorkingTreeChanged(paths: ReadonlySet<string>): void {
379
393
  this._updatedAt = Date.now();
380
394
 
@@ -384,10 +398,16 @@ export class Repository {
384
398
  }
385
399
 
386
400
  if (this._suspended) return;
387
-
388
- const e = this._pendingWorkingTreeChange;
389
- this._pendingWorkingTreeChange = undefined;
390
- this._onDidChangeWorkingTree.fire(e);
401
+ if (this._pendingWorkingTreeFlush) return;
402
+
403
+ this._pendingWorkingTreeFlush = true;
404
+ queueMicrotask(() => {
405
+ this._pendingWorkingTreeFlush = false;
406
+ const e = this._pendingWorkingTreeChange;
407
+ if (e == null || this._suspended) return;
408
+ this._pendingWorkingTreeChange = undefined;
409
+ this._onDidChangeWorkingTree.fire(e);
410
+ });
391
411
  }
392
412
 
393
413
  @trace({ onlyExit: true })
@@ -135,10 +135,10 @@ export class GitWorktree {
135
135
  : GitWorktree.formatDateFromNow(worktree);
136
136
  }
137
137
 
138
- static async getStatus(worktree: GitWorktree): Promise<GitStatus | undefined> {
138
+ static async getStatus(worktree: GitWorktree, cancellation?: AbortSignal): Promise<GitStatus | undefined> {
139
139
  if (worktree.type === 'bare') return undefined;
140
140
  const repo = getRepositoryService(worktree.path);
141
- return repo?.status.getStatus();
141
+ return repo?.status.getStatus(cancellation);
142
142
  }
143
143
 
144
144
  static async hasWorkingChanges(
@@ -364,43 +364,6 @@ export function parseGitFileDiff(data: string, includeRawContent = false): Parse
364
364
  };
365
365
  }
366
366
 
367
- export function parseGitDiffNameStatusFiles(data: string, repoPath: string): GitFile[] | undefined {
368
- using sw = maybeStopWatch('Git.parseDiffNameStatusFiles', { log: { onlyExit: true, level: 'debug' } });
369
- if (!data) {
370
- sw?.stop({ suffix: ` no data` });
371
- return undefined;
372
- }
373
-
374
- const files: GitFile[] = [];
375
-
376
- let status;
377
-
378
- const fields = data.split('\0');
379
- for (let i = 0; i < fields.length - 1; i++) {
380
- status = fields[i][0];
381
- if (status === '.') {
382
- status = '?';
383
- }
384
-
385
- let originalPath;
386
- // Renamed files are old followed by the new path
387
- if (status === 'R' || status === 'C') {
388
- if (i + 1 >= fields.length - 1) break;
389
-
390
- originalPath = fields[++i];
391
- }
392
- if (i + 1 >= fields.length) break;
393
-
394
- const path = fields[++i];
395
-
396
- files.push({ status: status as GitFileStatus, path: path, originalPath: originalPath, repoPath: repoPath });
397
- }
398
-
399
- sw?.stop({ suffix: ` parsed ${files.length} files` });
400
-
401
- return files;
402
- }
403
-
404
367
  /**
405
368
  * Parses the output of `git diff --numstat --summary -z`.
406
369
  *
@@ -477,7 +440,7 @@ export function parseGitDiffNumStatFiles(data: string, repoPath: string): GitFil
477
440
  repoPath: repoPath,
478
441
  path: path,
479
442
  originalPath: originalPath,
480
- status: (originalPath ? 'R' : 'M') as GitFileStatus,
443
+ status: originalPath ? 'R' : 'M',
481
444
  stats: { additions: additions, deletions: deletions, changes: additions + deletions },
482
445
  });
483
446
 
@@ -514,14 +477,14 @@ export function parseGitDiffNumStatFiles(data: string, repoPath: string): GitFil
514
477
  if (createOrDelete != null) {
515
478
  const file = files.get(normalizePath(createOrDeletePath));
516
479
  if (file != null) {
517
- file.status = (createOrDelete === 'create' ? 'A' : 'D') as GitFileStatus;
480
+ file.status = createOrDelete === 'create' ? 'A' : 'D';
518
481
  }
519
482
  } else if (renameOrCopy === 'copy') {
520
483
  // Numstat defaulted to R; promote to C. Reconstruct the full new-side path by
521
484
  // concatenating prefix + new-leaf + suffix, covering all compact and non-compact forms.
522
485
  const file = files.get(normalizePath(renameRoot + renameNewLeaf + renameSuffix));
523
486
  if (file != null) {
524
- file.status = 'C' as GitFileStatus;
487
+ file.status = 'C';
525
488
  }
526
489
  } else if (modeChangePath != null) {
527
490
  // File-type boundary: 100xxx = regular file, 120000 = symlink, 160000 = gitlink.
@@ -529,7 +492,7 @@ export function parseGitDiffNumStatFiles(data: string, repoPath: string): GitFil
529
492
  if (oldMode.substring(0, 2) !== newMode.substring(0, 2)) {
530
493
  const file = files.get(normalizePath(modeChangePath));
531
494
  if (file != null) {
532
- file.status = 'T' as GitFileStatus;
495
+ file.status = 'T';
533
496
  }
534
497
  }
535
498
  }
@@ -564,7 +527,7 @@ export function parseGitApplyFiles(data: string, repoPath: string): GitFileChang
564
527
  const [insertions, deletions, path] = line.split('\t');
565
528
  files.set(
566
529
  normalizePath(path),
567
- new GitFileChange(repoPath, path, 'M' as GitFileStatus, getUri(path), undefined, undefined, undefined, {
530
+ new GitFileChange(repoPath, path, 'M', getUri(path), undefined, undefined, undefined, {
568
531
  changes: 0,
569
532
  additions: parseInt(insertions, 10),
570
533
  deletions: parseInt(deletions, 10),
@@ -603,7 +566,7 @@ export function parseGitApplyFiles(data: string, repoPath: string): GitFileChang
603
566
  new GitFileChange(
604
567
  repoPath,
605
568
  newPath,
606
- (renameOrCopy === 'copy' ? 'C' : 'R') as GitFileStatus,
569
+ renameOrCopy === 'copy' ? 'C' : 'R',
607
570
  getUri(newPath),
608
571
  originalPath,
609
572
  getUri(originalPath),
@@ -618,7 +581,7 @@ export function parseGitApplyFiles(data: string, repoPath: string): GitFileChang
618
581
  new GitFileChange(
619
582
  repoPath,
620
583
  file.path,
621
- (createOrDelete === 'create' ? 'A' : 'D') as GitFileStatus,
584
+ createOrDelete === 'create' ? 'A' : 'D',
622
585
  getUri(file.path),
623
586
  undefined,
624
587
  undefined,