@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
@@ -6,7 +6,7 @@ import { deletedOrMissing } from '../../git/models/revision.js';
6
6
  import type { GitTag } from '../../git/models/tag.js';
7
7
  import type { GitRefsSubProvider } from '../../git/providers/refs.js';
8
8
  import { createReference } from '../../git/utils/reference.utils.js';
9
- import { isShaWithOptionalRevisionSuffix, isUncommitted } from '../../git/utils/revision.utils.js';
9
+ import { isSha, isShaWithOptionalRevisionSuffix, isUncommitted } from '../../git/utils/revision.utils.js';
10
10
  import { isCancellationError } from '../../utils/cancellation.js';
11
11
  import { debug, trace } from '../../utils/decorators/log.js';
12
12
  import { getScopedLogger } from '../../utils/logger.scoped.js';
@@ -26,7 +26,7 @@ export class RefsGitSubProvider implements GitRefsSubProvider {
26
26
  @debug()
27
27
  async checkIfCouldBeValidBranchOrTagName(repoPath: string, ref: string): Promise<boolean> {
28
28
  try {
29
- const result = await this.git.exec({ cwd: repoPath, errors: 'throw' }, 'check-ref-format', '--branch', ref);
29
+ const result = await this.git.run({ cwd: repoPath, errors: 'throw' }, 'check-ref-format', '--branch', ref);
30
30
  return Boolean(result.stdout.trim());
31
31
  } catch {
32
32
  return false;
@@ -44,10 +44,13 @@ export class RefsGitSubProvider implements GitRefsSubProvider {
44
44
  const scope = getScopedLogger();
45
45
 
46
46
  try {
47
- const result = await this.git.exec(
47
+ const result = await this.git.run(
48
48
  {
49
49
  cwd: repoPath,
50
50
  cancellation: cancellation,
51
+ // Why: ref1/ref2 are usually branch names; correctness relies on the gitResults cache being
52
+ // cleared on 'heads'/'remotes' events when refs move. Web (no fs watcher) sees up to
53
+ // `accessTTL` of staleness — acceptable trade-off for the perf win on graph/branch reads.
51
54
  caching: { cache: this.cache.gitResults, options: { accessTTL: 5 * 60 * 1000 } },
52
55
  },
53
56
  'merge-base',
@@ -105,8 +108,16 @@ export class RefsGitSubProvider implements GitRefsSubProvider {
105
108
  ): Promise<string | undefined> {
106
109
  const supportsEndOfOptions = await this.git.supports('git:rev-parse:end-of-options');
107
110
 
108
- const result = await this.git.exec(
109
- { cwd: repoPath, cancellation: cancellation, errors: 'ignore' },
111
+ const result = await this.git.run(
112
+ {
113
+ cwd: repoPath,
114
+ cancellation: cancellation,
115
+ errors: 'ignore',
116
+ // Why: a fixed ref name's symbolic name is itself stable; only HEAD is mutable, and the
117
+ // gitResults cache is cleared on 'head' events. 60s TTL is the failsafe for watcher
118
+ // latency / web — matches the other "resolve symbolic state" calls in commits.ts.
119
+ caching: { cache: this.cache.gitResults, options: { accessTTL: 60 * 1000 } },
120
+ },
110
121
  'rev-parse',
111
122
  '--verify',
112
123
  '--quiet',
@@ -164,12 +175,20 @@ export class RefsGitSubProvider implements GitRefsSubProvider {
164
175
 
165
176
  const supportsEndOfOptions = await this.git.supports('git:rev-parse:end-of-options');
166
177
 
167
- const result = await this.git.exec(
178
+ // Why: a SHA-only validation (no path suffix) is effectively immutable — 5-min TTL is safe.
179
+ // Otherwise the resolved SHA can shift on ref move (or working-tree change for path-scoped
180
+ // validation); rely on gitResults being cleared on 'head'/'heads'/'remotes' events, with 60s
181
+ // TTL as the failsafe for watcher latency / web.
182
+ const stable = relativePath == null && isSha(ref);
183
+ const result = await this.git.run(
168
184
  {
169
185
  cwd: repoPath,
170
186
  cancellation: cancellation,
171
187
  errors: 'ignore',
172
- caching: { cache: this.cache.gitResults, options: { accessTTL: 5 * 60 * 1000 } },
188
+ caching: {
189
+ cache: this.cache.gitResults,
190
+ options: { accessTTL: stable ? 5 * 60 * 1000 : 60 * 1000 },
191
+ },
173
192
  },
174
193
  'rev-parse',
175
194
  '--verify',
@@ -184,7 +203,7 @@ export class RefsGitSubProvider implements GitRefsSubProvider {
184
203
  const scope = getScopedLogger();
185
204
 
186
205
  try {
187
- await this.git.exec({ cwd: repoPath, cancellation: cancellation }, 'update-ref', ref, newRef);
206
+ await this.git.run({ cwd: repoPath, cancellation: cancellation }, 'update-ref', ref, newRef);
188
207
  } catch (ex) {
189
208
  scope?.error(ex);
190
209
  if (isCancellationError(ex)) throw ex;
@@ -41,7 +41,7 @@ export class RemotesGitSubProvider extends RemotesGitProviderBase implements Git
41
41
  const remoteProviderMatcher = createRemoteProviderMatcher(configs, this.context.remotes);
42
42
 
43
43
  try {
44
- const result = await this.git.exec(
44
+ const result = await this.git.run(
45
45
  {
46
46
  cwd: repoPath,
47
47
  cancellation: signal,
@@ -79,7 +79,7 @@ export class RemotesGitSubProvider extends RemotesGitProviderBase implements Git
79
79
  @gate()
80
80
  @debug()
81
81
  async addRemote(repoPath: string, name: string, url: string, options?: { fetch?: boolean }): Promise<void> {
82
- await this.git.exec({ cwd: repoPath }, 'remote', 'add', options?.fetch ? '-f' : undefined, name, url);
82
+ await this.git.run({ cwd: repoPath }, 'remote', 'add', options?.fetch ? '-f' : undefined, name, url);
83
83
  this.context.hooks?.cache?.onReset?.(repoPath, 'remotes');
84
84
  this.context.hooks?.repository?.onChanged?.(repoPath, ['remotes']);
85
85
  }
@@ -100,7 +100,7 @@ export class RemotesGitSubProvider extends RemotesGitProviderBase implements Git
100
100
  @gate()
101
101
  @debug()
102
102
  async pruneRemote(repoPath: string, name: string): Promise<void> {
103
- await this.git.exec({ cwd: repoPath }, 'remote', 'prune', name);
103
+ await this.git.run({ cwd: repoPath }, 'remote', 'prune', name);
104
104
  this.context.hooks?.cache?.onReset?.(repoPath, 'remotes', 'branches');
105
105
  this.context.hooks?.repository?.onChanged?.(repoPath, ['remotes']);
106
106
  }
@@ -108,7 +108,7 @@ export class RemotesGitSubProvider extends RemotesGitProviderBase implements Git
108
108
  @gate()
109
109
  @debug()
110
110
  async removeRemote(repoPath: string, name: string): Promise<void> {
111
- await this.git.exec({ cwd: repoPath }, 'remote', 'remove', name);
111
+ await this.git.run({ cwd: repoPath }, 'remote', 'remove', name);
112
112
  this.context.hooks?.cache?.onReset?.(repoPath, 'remotes');
113
113
  this.context.hooks?.repository?.onChanged?.(repoPath, ['remotes']);
114
114
  }
@@ -36,16 +36,20 @@ export class RevisionGitSubProvider implements GitRevisionSubProvider {
36
36
  private readonly provider: CliGitProviderInternal,
37
37
  ) {}
38
38
 
39
- exists(repoPath: string, path: string, revOrOptions?: string | { untracked?: boolean }): Promise<boolean> {
39
+ exists(
40
+ repoPath: string,
41
+ path: string,
42
+ revOrOptions?: string | { untracked?: 'only' | 'include' },
43
+ ): Promise<boolean> {
40
44
  let rev: string | undefined;
41
- let untracked: boolean | undefined;
45
+ let untracked: 'only' | 'include' | undefined;
42
46
  if (typeof revOrOptions === 'string') {
43
47
  rev = revOrOptions;
44
48
  } else if (revOrOptions != null) {
45
49
  untracked = revOrOptions.untracked;
46
50
  }
47
51
 
48
- const cacheKey = `${path}\0${rev ?? ''}\0${untracked ? 'u' : ''}`;
52
+ const cacheKey = `${path}\0${rev ?? ''}\0${untracked ?? ''}`;
49
53
  return this.cache.fileExistence.getOrCreate(repoPath, cacheKey, async () => {
50
54
  const args = ['ls-files'];
51
55
  if (rev) {
@@ -54,11 +58,14 @@ export class RevisionGitSubProvider implements GitRevisionSubProvider {
54
58
  } else if (isUncommittedStaged(rev)) {
55
59
  args.push('--stage');
56
60
  }
57
- } else if (untracked) {
61
+ } else if (untracked === 'only') {
58
62
  args.push('-o');
63
+ } else if (untracked === 'include') {
64
+ // Tracked (cached) OR untracked (others)
65
+ args.push('-c', '-o');
59
66
  }
60
67
 
61
- const result = await this.git.exec({ cwd: repoPath, errors: 'ignore' }, ...args, '--', path);
68
+ const result = await this.git.run({ cwd: repoPath, errors: 'ignore' }, ...args, '--', path);
62
69
  return Boolean(result.stdout.trim());
63
70
  });
64
71
  }
@@ -79,7 +86,7 @@ export class RevisionGitSubProvider implements GitRevisionSubProvider {
79
86
  const [relativePath, root] = splitPath(submodulePath, repoPath);
80
87
  const submoduleFullPath = this.provider.getAbsoluteUri(relativePath, root).fsPath;
81
88
 
82
- const result = await this.git.exec({ cwd: submoduleFullPath, errors: 'ignore' }, 'rev-parse', 'HEAD');
89
+ const result = await this.git.run({ cwd: submoduleFullPath, errors: 'ignore' }, 'rev-parse', 'HEAD');
83
90
  const sha = result.stdout.trim();
84
91
  return sha || undefined;
85
92
  }
@@ -92,7 +99,7 @@ export class RevisionGitSubProvider implements GitRevisionSubProvider {
92
99
  const [relativePath, root] = splitPath(path, repoPath);
93
100
 
94
101
  if (isUncommittedStaged(rev)) {
95
- let result = await this.git.exec(
102
+ let result = await this.git.run(
96
103
  { cwd: root, errors: 'ignore' },
97
104
  'ls-files',
98
105
  '-z',
@@ -105,7 +112,7 @@ export class RevisionGitSubProvider implements GitRevisionSubProvider {
105
112
  if (entries.length === 0) return undefined;
106
113
 
107
114
  const entry = entries[0];
108
- result = await this.git.exec({ cwd: root }, 'cat-file', '-s', entry.oid);
115
+ result = await this.git.run({ cwd: root }, 'cat-file', '-s', entry.oid);
109
116
  const size = parseInt(result.stdout.trim(), 10) || 0;
110
117
 
111
118
  return { ref: rev, oid: entry.oid, path: relativePath, size: size, type: 'blob' };
@@ -119,7 +126,7 @@ export class RevisionGitSubProvider implements GitRevisionSubProvider {
119
126
  async getTrackedFiles(repoPath: string): Promise<string[]> {
120
127
  if (!repoPath) return [];
121
128
 
122
- const result = await this.git.exec({ cwd: repoPath, errors: 'ignore' }, 'ls-files', '-z');
129
+ const result = await this.git.run({ cwd: repoPath, errors: 'ignore' }, 'ls-files', '-z');
123
130
  const data = result.stdout;
124
131
  if (!data) return [];
125
132
 
@@ -135,7 +142,7 @@ export class RevisionGitSubProvider implements GitRevisionSubProvider {
135
142
  private async getTreeForRevisionCore(repoPath: string, rev: string, path?: string): Promise<GitTreeEntry[]> {
136
143
  const hasPath = Boolean(path);
137
144
  const args = hasPath ? ['ls-tree', '-l', rev, '--', path] : ['ls-tree', '-lrt', rev, '--'];
138
- const result = await this.git.exec({ cwd: repoPath, errors: 'ignore' }, ...args);
145
+ const result = await this.git.run({ cwd: repoPath, errors: 'ignore' }, ...args);
139
146
  const data = result.stdout.trim();
140
147
  if (!data) return emptyArray as GitTreeEntry[];
141
148
 
@@ -168,7 +175,7 @@ export class RevisionGitSubProvider implements GitRevisionSubProvider {
168
175
  const params = ['show', '--textconv', args, '--'];
169
176
 
170
177
  try {
171
- const result = await this.git.exec<T>(opts, ...params);
178
+ const result = await this.git.run<T>(opts, ...params);
172
179
  return result.stdout;
173
180
  } catch (ex) {
174
181
  const msg: string = ex?.toString() ?? '';
@@ -261,7 +268,7 @@ export class RevisionGitSubProvider implements GitRevisionSubProvider {
261
268
  const resolvedRevisionCaching = { cache: this.cache.gitResults, options: { accessTTL: 5 * 60 * 1000 } };
262
269
 
263
270
  const parser = getShaAndFileSummaryLogParser();
264
- let result = await this.git.exec(
271
+ let result = await this.git.run(
265
272
  { cwd: repoPath, errors: 'ignore', caching: resolvedRevisionCaching },
266
273
  'log',
267
274
  ...parser.arguments,
@@ -293,7 +300,7 @@ export class RevisionGitSubProvider implements GitRevisionSubProvider {
293
300
 
294
301
  if (file.status === 'D') {
295
302
  // If the file was deleted, check if it was moved or renamed
296
- result = await this.git.exec(
303
+ result = await this.git.run(
297
304
  { cwd: repoPath, errors: 'ignore', caching: resolvedRevisionCaching },
298
305
  'log',
299
306
  ...parser.arguments,
@@ -64,7 +64,7 @@ export class StagingGitSubProvider implements GitStagingSubProvider {
64
64
  if (ref == null) throw new Error(`ref is required when from is 'ref'`);
65
65
 
66
66
  // Create the temp index file from a base ref/sha
67
- const newIndexResult = await this.git.exec(
67
+ const newIndexResult = await this.git.run(
68
68
  { cwd: repoPath, env: env },
69
69
  'ls-tree',
70
70
  '-z',
@@ -75,7 +75,7 @@ export class StagingGitSubProvider implements GitStagingSubProvider {
75
75
 
76
76
  if (newIndexResult.stdout.trim()) {
77
77
  // Write the tree to our temp index
78
- await this.git.exec(
78
+ await this.git.run(
79
79
  { cwd: repoPath, env: env, stdin: newIndexResult.stdout },
80
80
  'update-index',
81
81
  '-z',
@@ -98,7 +98,7 @@ export class StagingGitSubProvider implements GitStagingSubProvider {
98
98
 
99
99
  @debug()
100
100
  async stageFile(repoPath: string, pathOrUri: string | Uri): Promise<void> {
101
- await this.git.exec({ cwd: repoPath }, 'add', '-A', '--', toFsPath(pathOrUri));
101
+ await this.git.run({ cwd: repoPath }, 'add', '-A', '--', toFsPath(pathOrUri));
102
102
  }
103
103
 
104
104
  @debug()
@@ -117,7 +117,7 @@ export class StagingGitSubProvider implements GitStagingSubProvider {
117
117
  // Process files in batches (will be a single batch if under the limit)
118
118
  const batches = chunk(paths, batchSize);
119
119
  for (const batch of batches) {
120
- await this.git.exec(
120
+ await this.git.run(
121
121
  { cwd: repoPath, env: options?.index?.env },
122
122
  'add',
123
123
  options?.intentToAdd ? '-N' : '-A',
@@ -129,12 +129,12 @@ export class StagingGitSubProvider implements GitStagingSubProvider {
129
129
 
130
130
  @debug()
131
131
  async stageDirectory(repoPath: string, directoryOrUri: string | Uri): Promise<void> {
132
- await this.git.exec({ cwd: repoPath }, 'add', '-A', '--', toFsPath(directoryOrUri));
132
+ await this.git.run({ cwd: repoPath }, 'add', '-A', '--', toFsPath(directoryOrUri));
133
133
  }
134
134
 
135
135
  @debug()
136
136
  async unstageFile(repoPath: string, pathOrUri: string | Uri): Promise<void> {
137
- await this.git.exec({ cwd: repoPath }, 'reset', '-q', '--', toFsPath(pathOrUri));
137
+ await this.git.run({ cwd: repoPath }, 'reset', '-q', '--', toFsPath(pathOrUri));
138
138
  }
139
139
 
140
140
  @debug()
@@ -149,12 +149,22 @@ export class StagingGitSubProvider implements GitStagingSubProvider {
149
149
  // Process files in batches (will be a single batch if under the limit)
150
150
  const batches = chunk(paths, batchSize);
151
151
  for (const batch of batches) {
152
- await this.git.exec({ cwd: repoPath }, 'reset', '-q', '--', ...batch);
152
+ await this.git.run({ cwd: repoPath }, 'reset', '-q', '--', ...batch);
153
153
  }
154
154
  }
155
155
 
156
156
  @debug()
157
157
  async unstageDirectory(repoPath: string, directoryOrUri: string | Uri): Promise<void> {
158
- await this.git.exec({ cwd: repoPath }, 'reset', '-q', '--', toFsPath(directoryOrUri));
158
+ await this.git.run({ cwd: repoPath }, 'reset', '-q', '--', toFsPath(directoryOrUri));
159
+ }
160
+
161
+ @debug()
162
+ async stageAll(repoPath: string): Promise<void> {
163
+ await this.git.run({ cwd: repoPath }, 'add', '-A');
164
+ }
165
+
166
+ @debug()
167
+ async unstageAll(repoPath: string): Promise<void> {
168
+ await this.git.run({ cwd: repoPath }, 'reset', '-q');
159
169
  }
160
170
  }
@@ -1,7 +1,7 @@
1
1
  import type { Cache } from '../../git/cache.js';
2
2
  import type { GitServiceContext } from '../../git/context.js';
3
3
  import { StashApplyError, StashPushError } from '../../git/errors.js';
4
- import type { GitStashCommit, GitStashParentInfo } from '../../git/models/commit.js';
4
+ import type { GitStashCommit } from '../../git/models/commit.js';
5
5
  import { GitCommit, GitCommitIdentity } from '../../git/models/commit.js';
6
6
  import { GitFileChange } from '../../git/models/fileChange.js';
7
7
  import type { GitFileStatus } from '../../git/models/fileStatus.js';
@@ -12,8 +12,7 @@ import type { GitStashSubProvider, StashApplyResult } from '../../git/providers/
12
12
  import { countStringLength } from '../../utils/array.js';
13
13
  import { gate } from '../../utils/decorators/gate.js';
14
14
  import { debug } from '../../utils/decorators/log.js';
15
- import { min, skip } from '../../utils/iterable.js';
16
- import { Logger } from '../../utils/logger.js';
15
+ import { skip } from '../../utils/iterable.js';
17
16
  import { normalizePath, splitPath } from '../../utils/path.js';
18
17
  import { getSettledValue } from '../../utils/promise.js';
19
18
  import type { Uri } from '../../utils/uri.js';
@@ -23,7 +22,7 @@ import { gitFeaturesByVersion } from '../exec/features.js';
23
22
  import type { Git } from '../exec/git.js';
24
23
  import { getGitCommandError, GitError, GitErrors, maxGitCliLength } from '../exec/git.js';
25
24
  import type { ParsedStash, ParsedStashWithFiles } from '../parsers/logParser.js';
26
- import { getShaAndDatesLogParser, getStashFilesOnlyLogParser, getStashLogParser } from '../parsers/logParser.js';
25
+ import { getStashFilesOnlyLogParser, getStashLogParser } from '../parsers/logParser.js';
27
26
  import { createCommitFileset } from './commitFilesetUtils.js';
28
27
 
29
28
  export class StashGitSubProvider implements GitStashSubProvider {
@@ -57,7 +56,7 @@ export class StashGitSubProvider implements GitStashSubProvider {
57
56
  conflictCommand: 'stash-apply' | 'stash-pop',
58
57
  ): Promise<StashApplyResult> {
59
58
  try {
60
- await this.git.exec({ cwd: repoPath }, ...args);
59
+ await this.git.run({ cwd: repoPath }, ...args);
61
60
  this.context.hooks?.cache?.onReset?.(repoPath, 'stashes');
62
61
  this.context.hooks?.repository?.onChanged?.(repoPath, ['stash']);
63
62
  return { conflicted: false };
@@ -94,7 +93,7 @@ export class StashGitSubProvider implements GitStashSubProvider {
94
93
  args.push(message);
95
94
  }
96
95
 
97
- const result = await this.git.exec({ cwd: repoPath }, ...args);
96
+ const result = await this.git.run({ cwd: repoPath }, ...args);
98
97
  return result.stdout.trim() || undefined;
99
98
  }
100
99
 
@@ -110,135 +109,81 @@ export class StashGitSubProvider implements GitStashSubProvider {
110
109
  ): Promise<GitStash | undefined> {
111
110
  if (repoPath == null) return undefined;
112
111
 
112
+ // Resolve options against config up front so the cache key reflects the actual values used
113
+ // to produce the cached payload — undefined inputs aren't conflated across config changes.
113
114
  const cfg = this.context.config;
115
+ const includeFiles = options?.includeFiles ?? cfg?.commits.includeFileDetails ?? true;
116
+ const similarityThreshold = includeFiles
117
+ ? (options?.similarityThreshold ?? cfg?.commits.similarityThreshold ?? null)
118
+ : null;
119
+ const reachableFrom = options?.reachableFrom;
120
+
121
+ // Cache only the unfiltered stash list; reachability projection runs below by testing each
122
+ // distinct stash parent against `reachableFrom` directly, so re-projecting is cheap
123
+ const cacheKey = `f${includeFiles ? 1 : 0}|s${similarityThreshold ?? ''}`;
124
+
114
125
  const stash = await this.cache.getStash(
115
126
  repoPath,
127
+ cacheKey,
116
128
  async (commonPath, _cacheable, signal) => {
117
129
  // Prefer the aggregate signal from the cache; fall back to the caller's cancellation.
118
130
  signal ??= cancellation;
119
- const includeFiles = options?.includeFiles ?? cfg?.commits.includeFileDetails ?? true;
120
131
  const parser = getStashLogParser(includeFiles);
121
132
  const args = [...parser.arguments];
122
133
 
123
134
  if (includeFiles) {
124
- const similarityThreshold = options?.similarityThreshold ?? cfg?.commits.similarityThreshold;
125
135
  args.push(`-M${similarityThreshold == null ? '' : `${similarityThreshold}%`}`);
126
136
  }
127
137
 
128
- const result = await this.git.exec({ cwd: repoPath, cancellation: signal }, 'stash', 'list', ...args);
138
+ const result = await this.git.run({ cwd: repoPath, cancellation: signal }, 'stash', 'list', ...args);
129
139
 
130
140
  const currentUser = await this.provider.config.getCurrentUser(repoPath);
131
141
 
132
142
  const stashes = new Map<string, GitStashCommit>();
133
- const parentShas = new Set<string>();
134
-
135
- // First pass: create stashes and collect parent SHAs
136
143
  for (const s of parser.parse(result.stdout)) {
137
144
  stashes.set(s.sha, createStash(s, commonPath, currentUser));
138
- // Collect all parent SHAs for timestamp lookup
139
- if (s.parents) {
140
- for (const parentSha of s.parents.split(' ')) {
141
- if (parentSha.trim()) {
142
- parentShas.add(parentSha.trim());
143
- }
144
- }
145
- }
146
- }
147
-
148
- // Second pass: fetch parent timestamps if we have any parents
149
- const parentTimestamps = new Map<string, { authorDate: number; committerDate: number }>();
150
- if (parentShas.size > 0) {
151
- try {
152
- const datesParser = getShaAndDatesLogParser();
153
- const parentResult = await this.git.exec(
154
- {
155
- cwd: repoPath,
156
- cancellation: signal,
157
- stdin: [...parentShas].join('\n'),
158
- },
159
- 'log',
160
- ...datesParser.arguments,
161
- '--no-walk',
162
- '--stdin',
163
- );
164
-
165
- for (const entry of datesParser.parse(parentResult.stdout)) {
166
- parentTimestamps.set(entry.sha, {
167
- authorDate: Number(entry.authorDate),
168
- committerDate: Number(entry.committerDate),
169
- });
170
- }
171
- } catch (ex) {
172
- Logger.debug(`Failed to fetch parent timestamps for stash commits: ${ex}`);
173
- }
174
- }
175
-
176
- // Third pass: update stashes with parent timestamp information
177
- for (const sha of stashes.keys()) {
178
- const stashCommit = stashes.get(sha);
179
- if (stashCommit?.parents.length) {
180
- const parentsWithTimestamps: GitStashParentInfo[] = stashCommit.parents.map(parentSha => ({
181
- sha: parentSha,
182
- authorDate: parentTimestamps.get(parentSha)?.authorDate,
183
- committerDate: parentTimestamps.get(parentSha)?.committerDate,
184
- }));
185
- // Store the parent timestamp information on the stash
186
- stashes.set(sha, stashCommit.with({ parentTimestamps: parentsWithTimestamps }));
187
- }
188
145
  }
189
146
 
190
147
  return { repoPath: commonPath, stashes: stashes };
191
148
  },
192
- cancellation,
149
+ { cancellation: cancellation },
193
150
  );
194
151
 
195
- if (stash == null) return undefined;
196
- if (!options?.reachableFrom || !stash?.stashes.size) return stash;
152
+ if (stash == null || !reachableFrom || !stash.stashes.size) return stash;
197
153
 
198
- // Return only reachable stashes from the given ref
199
- // Create a copy because we are going to modify it and we don't want to mutate the cache
154
+ // Copy because the cached `stash.stashes` map is shared and must not be mutated.
200
155
  const stashes = new Map(stash.stashes);
201
156
 
202
- const oldestStashDate = new Date(findOldestStashTimestamp(stash.stashes.values())).toISOString();
203
-
204
- const { reachableFrom } = options;
205
- const reachableShas = await this.provider.commits.getLogShas(repoPath, reachableFrom, {
206
- since: oldestStashDate,
207
- ordering: 'date',
208
- limit: 0,
209
- });
210
- const reachableCommits = new Set(reachableShas);
157
+ const reachableStashes = new Set<string>();
211
158
 
212
- if (reachableCommits.size) {
213
- const reachableStashes = new Set<string>();
159
+ // Metadata pass: a stash belongs on `reachableFrom` iff git's auto-prepended
160
+ // `On <branch>: …` (or `WIP on <branch>: …`) prefix in the stash commit subject names
161
+ // this branch. `stashOnRef` is parsed from %s in createStash, so this covers autostashes
162
+ // and custom-reflog stashes too.
163
+ for (const [sha, s] of stash.stashes) {
164
+ if (s.stashOnRef === reachableFrom) {
165
+ reachableStashes.add(sha);
166
+ }
167
+ }
214
168
 
215
- // First pass: mark directly reachable stashes
169
+ // Transitive pass for the rare detached-HEAD-on-stash chain (stash B created while HEAD
170
+ // pointed at stash A). Such stashes have no `stashOnRef`, but if their parent is a stash
171
+ // already marked reachable they should follow.
172
+ let changed;
173
+ do {
174
+ changed = false;
216
175
  for (const [sha, s] of stash.stashes) {
217
- if (s.parents.some(p => p === reachableFrom || reachableCommits.has(p))) {
176
+ if (!reachableStashes.has(sha) && s.parents.some(p => reachableStashes.has(p))) {
218
177
  reachableStashes.add(sha);
178
+ changed = true;
219
179
  }
220
180
  }
181
+ } while (changed);
221
182
 
222
- // Second pass: mark stashes that build upon reachable stashes
223
- let changed;
224
- do {
225
- changed = false;
226
- for (const [sha, s] of stash.stashes) {
227
- if (!reachableStashes.has(sha) && s.parents.some(p => reachableStashes.has(p))) {
228
- reachableStashes.add(sha);
229
- changed = true;
230
- }
231
- }
232
- } while (changed);
233
-
234
- // Remove unreachable stashes
235
- for (const [sha] of stash.stashes) {
236
- if (!reachableStashes.has(sha)) {
237
- stashes.delete(sha);
238
- }
183
+ for (const [sha] of stash.stashes) {
184
+ if (!reachableStashes.has(sha)) {
185
+ stashes.delete(sha);
239
186
  }
240
- } else {
241
- stashes.clear();
242
187
  }
243
188
 
244
189
  return { ...stash, stashes: stashes };
@@ -288,7 +233,7 @@ export class StashGitSubProvider implements GitStashSubProvider {
288
233
  }
289
234
 
290
235
  const parser = getStashFilesOnlyLogParser();
291
- const result = await this.git.exec(
236
+ const result = await this.git.run(
292
237
  { cwd: repoPath, cancellation: cancellation },
293
238
  'stash',
294
239
  'show',
@@ -343,7 +288,7 @@ export class StashGitSubProvider implements GitStashSubProvider {
343
288
 
344
289
  let result;
345
290
  if (sha) {
346
- result = await this.git.exec(
291
+ result = await this.git.run(
347
292
  { cwd: repoPath, errors: 'ignore' },
348
293
  'show',
349
294
  '--format=%H',
@@ -355,7 +300,7 @@ export class StashGitSubProvider implements GitStashSubProvider {
355
300
  }
356
301
  }
357
302
 
358
- result = await this.git.exec({ cwd: repoPath }, 'stash', 'drop', stashName);
303
+ result = await this.git.run({ cwd: repoPath }, 'stash', 'drop', stashName);
359
304
  return result.stdout;
360
305
  }
361
306
 
@@ -368,7 +313,7 @@ export class StashGitSubProvider implements GitStashSubProvider {
368
313
  stashOnRef?: string,
369
314
  ): Promise<void> {
370
315
  await this.deleteStashCore(repoPath, stashName, sha);
371
- await this.git.exec(
316
+ await this.git.run(
372
317
  { cwd: repoPath },
373
318
  'stash',
374
319
  'store',
@@ -478,7 +423,7 @@ export class StashGitSubProvider implements GitStashSubProvider {
478
423
  }
479
424
 
480
425
  try {
481
- const result = await this.git.exec({ cwd: repoPath, stdin: stdin }, ...params);
426
+ const result = await this.git.run({ cwd: repoPath, stdin: stdin }, ...params);
482
427
  if (GitErrors.stashNothingToSave.test(result.stdout)) {
483
428
  throw new StashPushError({
484
429
  reason: 'nothingToSave',
@@ -502,7 +447,7 @@ export class StashGitSubProvider implements GitStashSubProvider {
502
447
 
503
448
  @debug()
504
449
  async saveSnapshot(repoPath: string, message?: string): Promise<void> {
505
- const result = await this.git.exec({ cwd: repoPath }, 'stash', 'create');
450
+ const result = await this.git.run({ cwd: repoPath }, 'stash', 'create');
506
451
  const id = result.stdout.trim() || undefined;
507
452
  if (id == null) return;
508
453
 
@@ -510,7 +455,7 @@ export class StashGitSubProvider implements GitStashSubProvider {
510
455
  if (message) {
511
456
  args.push('-m', message);
512
457
  }
513
- await this.git.exec({ cwd: repoPath }, 'stash', 'store', ...args, id);
458
+ await this.git.run({ cwd: repoPath }, 'stash', 'store', ...args, id);
514
459
 
515
460
  this.context.hooks?.repository?.onChanged?.(repoPath, ['stash']);
516
461
  this.context.hooks?.cache?.onReset?.(repoPath, 'stashes');
@@ -557,18 +502,25 @@ function createStash(
557
502
  repoPath: string,
558
503
  currentUser: GitUser | undefined,
559
504
  ): GitStashCommit {
560
- let message = s.summary.trim();
561
-
562
- let onRef;
563
- let summary;
564
- const match = stashSummaryRegex.exec(message);
565
- if (match?.groups != null) {
566
- onRef = match.groups.onref;
567
- summary = match.groups.summary.trim();
505
+ // `onRef` comes from the commit subject (%s), which git always prefixes with `On <branch>: …`
506
+ // or `WIP on <branch>: …`. This holds even when the reflog message was customized
507
+ // (autostash, `git stash store -m "…"`), so it's a reliable source
508
+ let onRef: string | undefined;
509
+ const subjectMatch = stashSummaryRegex.exec((s.commitSubject ?? '').trim());
510
+ if (subjectMatch?.groups?.onref) {
511
+ onRef = subjectMatch.groups.onref;
512
+ }
568
513
 
514
+ // The user-visible display message is derived from the reflog subject (%gs) so that short
515
+ // custom messages like "wip", "q", or "show args" stay as the user wrote them instead of
516
+ // being replaced by the verbose git-auto subject
517
+ let message = s.summary.trim();
518
+ const reflogMatch = stashSummaryRegex.exec(message);
519
+ if (reflogMatch?.groups != null) {
520
+ const summary = reflogMatch.groups.summary.trim();
569
521
  if (summary.length === 0) {
570
522
  message = 'WIP';
571
- } else if (match.groups.wip) {
523
+ } else if (reflogMatch.groups.wip) {
572
524
  message = `WIP: ${summary}`;
573
525
  } else {
574
526
  message = summary;
@@ -605,22 +557,3 @@ function createStash(
605
557
  onRef,
606
558
  ) as GitStashCommit;
607
559
  }
608
-
609
- /**
610
- * Finds the oldest timestamp among stash commits and their parent commits.
611
- * This includes both the stash commit dates and all parent commit timestamps (author and committer dates).
612
- *
613
- * @param stashes - Collection of stash commits to analyze
614
- * @returns The oldest timestamp in milliseconds, or Infinity if no stashes provided
615
- */
616
- export function findOldestStashTimestamp(stashes: Iterable<GitStashCommit>): number {
617
- return min(stashes, c => {
618
- return Math.min(
619
- c.committer.date.getTime(),
620
- ...(c.parentTimestamps
621
- ?.flatMap(p => [p.authorDate, p.committerDate])
622
- .filter((x): x is number => x != null)
623
- .map(x => x * 1000) ?? []),
624
- );
625
- });
626
- }