@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
@@ -27,6 +27,7 @@ import type { DiffRange } from '../../git/providers/types.js';
27
27
  import { createUncommittedChangesCommit } from '../../git/utils/commit.utils.js';
28
28
  import { isRevisionRange, isSha, isUncommitted, isUncommittedStaged } from '../../git/utils/revision.utils.js';
29
29
  import { parseSearchQueryGitCommand } from '../../git/utils/search.utils.js';
30
+ import { compareReachableRefs } from '../../git/utils/sorting.js';
30
31
  import { isUserMatch } from '../../git/utils/user.utils.js';
31
32
  import { CancellationError, isCancellationError } from '../../utils/cancellation.js';
32
33
  import { debug, trace } from '../../utils/decorators/log.js';
@@ -49,7 +50,12 @@ import type {
49
50
  CommitsWithFilesLogParser,
50
51
  ParsedCommit,
51
52
  } from '../parsers/logParser.js';
52
- import { getCommitsLogParser, getShaAndFilesAndStatsLogParser, getShaLogParser } from '../parsers/logParser.js';
53
+ import {
54
+ getCommitsLogParser,
55
+ getShaAndDatesLogParser,
56
+ getShaAndFilesAndStatsLogParser,
57
+ getShaLogParser,
58
+ } from '../parsers/logParser.js';
53
59
  import { getReflogParser, parseGitRefLog } from '../parsers/reflogParser.js';
54
60
  import { parseSignatureOutput, signatureFormat } from '../parsers/signatureParser.js';
55
61
  import { createCommitFileset } from './commitFilesetUtils.js';
@@ -74,7 +80,7 @@ export class CommitsGitSubProvider implements GitCommitsSubProvider {
74
80
  message: string,
75
81
  cancellation?: AbortSignal,
76
82
  ): Promise<string> {
77
- const result = await this.git.exec(
83
+ const result = await this.git.run(
78
84
  { cwd: repoPath, cancellation: cancellation, errors: 'throw' },
79
85
  'commit-tree',
80
86
  tree,
@@ -105,7 +111,7 @@ export class CommitsGitSubProvider implements GitCommitsSubProvider {
105
111
 
106
112
  @debug({ exit: true })
107
113
  async getCommitCount(repoPath: string, rev: string, cancellation?: AbortSignal): Promise<number | undefined> {
108
- const result = await this.git.exec(
114
+ const result = await this.git.run(
109
115
  { cwd: repoPath, cancellation: cancellation, errors: 'ignore' },
110
116
  'rev-list',
111
117
  '--count',
@@ -123,11 +129,52 @@ export class CommitsGitSubProvider implements GitCommitsSubProvider {
123
129
  return isNaN(count) ? undefined : count;
124
130
  }
125
131
 
132
+ @debug({ exit: true })
133
+ async getCommitDates(
134
+ repoPath: string,
135
+ rev: string,
136
+ cancellation?: AbortSignal,
137
+ ): Promise<{ authorDate: Date; committerDate: Date } | undefined> {
138
+ const parser = getShaAndDatesLogParser();
139
+ const result = await this.git.run(
140
+ {
141
+ cwd: repoPath,
142
+ cancellation: cancellation,
143
+ errors: 'ignore',
144
+ // Why: full SHAs identify immutable commits (5-min TTL is safe). Non-SHA refs rely on
145
+ // gitResults being cleared on 'head'/'heads'/'remotes' events; the 60s TTL is the
146
+ // failsafe for watcher latency / web (no fs watcher).
147
+ caching: {
148
+ cache: this.cache.gitResults,
149
+ options: { accessTTL: isSha(rev) ? 5 * 60 * 1000 : 60 * 1000 },
150
+ },
151
+ },
152
+ 'log',
153
+ '-1',
154
+ ...parser.arguments,
155
+ rev,
156
+ );
157
+ if (result.cancelled || cancellation?.aborted) return undefined;
158
+
159
+ for (const entry of parser.parse(result.stdout)) {
160
+ const authorSeconds = Number(entry.authorDate);
161
+ const committerSeconds = Number(entry.committerDate);
162
+ if (!Number.isFinite(authorSeconds) || !Number.isFinite(committerSeconds)) return undefined;
163
+ return {
164
+ authorDate: new Date(authorSeconds * 1000),
165
+ committerDate: new Date(committerSeconds * 1000),
166
+ };
167
+ }
168
+ return undefined;
169
+ }
170
+
126
171
  @debug()
127
172
  async getCommitFiles(repoPath: string, rev: string, cancellation?: AbortSignal): Promise<GitFileChange[]> {
128
173
  const parser = getShaAndFilesAndStatsLogParser();
129
- const result = await this.git.exec(
130
- { cwd: repoPath, cancellation: cancellation, configs: gitConfigsLog },
174
+ const result = await this.git.run(
175
+ // Single-commit log is cheap and almost always serves a user-initiated read; override
176
+ // the inferred 'background' priority so it doesn't queue behind heavier graph/log work.
177
+ { cwd: repoPath, cancellation: cancellation, configs: gitConfigsLog, priority: 'normal' },
131
178
  'log',
132
179
  ...parser.arguments,
133
180
  '-n1',
@@ -204,7 +251,7 @@ export class CommitsGitSubProvider implements GitCommitsSubProvider {
204
251
  const getCore = async (cacheable?: CacheController) => {
205
252
  try {
206
253
  // Use for-each-ref with %(HEAD) to mark current branch with *
207
- const result = await this.git.exec(
254
+ const result = await this.git.run(
208
255
  { cwd: repoPath, cancellation: cancellation, errors: 'ignore' },
209
256
  'for-each-ref',
210
257
  '--contains',
@@ -255,8 +302,9 @@ export class CommitsGitSubProvider implements GitCommitsSubProvider {
255
302
  }
256
303
  }
257
304
 
258
- // Sort to move tags to the end, preserving order within each type
259
- refs.sort((a, b) => (a.refType !== b.refType ? (a.refType === 'tag' ? 1 : -1) : 0));
305
+ // git already returns sorted by -HEAD, -committerdate, -version:refname;
306
+ // compareReachableRefs refines by current-first / local-before-remote / tag-version.
307
+ refs.sort(compareReachableRefs);
260
308
 
261
309
  return { refs: refs };
262
310
  } catch (ex) {
@@ -305,7 +353,7 @@ export class CommitsGitSubProvider implements GitCommitsSubProvider {
305
353
  }
306
354
 
307
355
  try {
308
- const result = await this.git.exec(
356
+ const result = await this.git.run(
309
357
  { cwd: repoPath, cancellation: cancellation, configs: gitConfigsLog },
310
358
  'log',
311
359
  ...args,
@@ -363,7 +411,7 @@ export class CommitsGitSubProvider implements GitCommitsSubProvider {
363
411
  // Initial commit SHA is shared across all worktrees
364
412
  return this.cache.getInitialCommitSha(repoPath, async commonPath => {
365
413
  try {
366
- const result = await this.git.exec(
414
+ const result = await this.git.run(
367
415
  { cwd: commonPath, cancellation: cancellation, errors: 'ignore' },
368
416
  'rev-list',
369
417
  `--max-parents=0`,
@@ -395,7 +443,7 @@ export class CommitsGitSubProvider implements GitCommitsSubProvider {
395
443
  : [];
396
444
 
397
445
  const run = async (): Promise<LeftRightCommitCountResult | undefined> => {
398
- const result = await this.git.exec(
446
+ const result = await this.git.run(
399
447
  { cwd: repoPath, cancellation: cancellation, errors: 'ignore' },
400
448
  'rev-list',
401
449
  '--left-right',
@@ -585,10 +633,15 @@ export class CommitsGitSubProvider implements GitCommitsSubProvider {
585
633
  cancellation: cancellation,
586
634
  configs: gitConfigsLogWithFiles,
587
635
  stdin: stdin,
636
+ // Single-commit log is cheap (typically <50ms) and almost always serves a user-
637
+ // initiated read (commit details, hover, etc.). Override the inferred 'background'
638
+ // priority so it doesn't queue behind heavier graph/log work — without this, a click
639
+ // on a commit while the graph is still loading can wait seconds for files to appear.
640
+ ...(isSingleCommit ? { priority: 'normal' as const } : undefined),
588
641
  };
589
642
  let { commits, count, countStashChildCommits } = await parseCommits(
590
643
  parser,
591
- isSingleCommit ? this.git.exec(cmdOpts, ...args) : this.git.stream(cmdOpts, ...args),
644
+ isSingleCommit ? this.git.run(cmdOpts, ...args) : this.git.stream(cmdOpts, ...args),
592
645
  repoPath,
593
646
  pathspec,
594
647
  limit,
@@ -616,7 +669,7 @@ export class CommitsGitSubProvider implements GitCommitsSubProvider {
616
669
 
617
670
  ({ commits, count, countStashChildCommits } = await parseCommits(
618
671
  parser,
619
- isSingleCommit ? this.git.exec(cmdOpts, ...args) : this.git.stream(cmdOpts, ...args),
672
+ isSingleCommit ? this.git.run(cmdOpts, ...args) : this.git.stream(cmdOpts, ...args),
620
673
  repoPath,
621
674
  pathspec,
622
675
  limit,
@@ -1058,7 +1111,7 @@ export class CommitsGitSubProvider implements GitCommitsSubProvider {
1058
1111
  args.push(pathspec);
1059
1112
  }
1060
1113
 
1061
- const result = await this.git.exec(
1114
+ const result = await this.git.run(
1062
1115
  { cwd: repoPath, cancellation: cancellation, configs: gitConfigsLog },
1063
1116
  'log',
1064
1117
  ...args,
@@ -1100,7 +1153,7 @@ export class CommitsGitSubProvider implements GitCommitsSubProvider {
1100
1153
 
1101
1154
  const parser = getShaLogParser();
1102
1155
 
1103
- const result = await this.git.exec(
1156
+ const result = await this.git.run(
1104
1157
  { cwd: repoPath, cancellation: cancellation, configs: gitConfigsLog },
1105
1158
  'log',
1106
1159
  ...parser.arguments,
@@ -1131,7 +1184,7 @@ export class CommitsGitSubProvider implements GitCommitsSubProvider {
1131
1184
  async isAncestorOf(repoPath: string, rev1: string, rev2: string, cancellation?: AbortSignal): Promise<boolean> {
1132
1185
  if (repoPath == null) return false;
1133
1186
 
1134
- const result = await this.git.exec(
1187
+ const result = await this.git.run(
1135
1188
  { cwd: repoPath, cancellation: cancellation, errors: 'ignore' },
1136
1189
  'merge-base',
1137
1190
  '--is-ancestor',
@@ -1310,7 +1363,7 @@ export class CommitsGitSubProvider implements GitCommitsSubProvider {
1310
1363
  const scope = getScopedLogger();
1311
1364
 
1312
1365
  try {
1313
- const result = await this.git.exec(
1366
+ const result = await this.git.run(
1314
1367
  {
1315
1368
  cwd: repoPath,
1316
1369
  errors: 'ignore',
@@ -1337,7 +1390,7 @@ export class CommitsGitSubProvider implements GitCommitsSubProvider {
1337
1390
  const scope = getScopedLogger();
1338
1391
 
1339
1392
  try {
1340
- const result = await this.git.exec(
1393
+ const result = await this.git.run(
1341
1394
  {
1342
1395
  cwd: repoPath,
1343
1396
  errors: 'ignore',
@@ -99,7 +99,7 @@ export class ConfigGitSubProvider implements GitConfigSubProvider {
99
99
  }
100
100
  args.push(key);
101
101
 
102
- const result = await this.git.exec(
102
+ const result = await this.git.run(
103
103
  { cwd: repoPath ?? '', errors: 'ignore', runLocally: options?.runGitLocally },
104
104
  ...args,
105
105
  );
@@ -131,7 +131,7 @@ export class ConfigGitSubProvider implements GitConfigSubProvider {
131
131
  }
132
132
  args.push(pattern);
133
133
 
134
- const result = await this.git.exec(
134
+ const result = await this.git.run(
135
135
  { cwd: repoPath ?? '', errors: 'ignore', runLocally: options?.runGitLocally },
136
136
  ...args,
137
137
  );
@@ -169,7 +169,7 @@ export class ConfigGitSubProvider implements GitConfigSubProvider {
169
169
  args.push(key, value);
170
170
  }
171
171
 
172
- await this.git.exec({ cwd: repoPath ?? '', runLocally: true }, ...args);
172
+ await this.git.run({ cwd: repoPath ?? '', runLocally: true }, ...args);
173
173
 
174
174
  // Only invalidate cache when not using a custom file (custom files aren't cached)
175
175
  if (!options?.file) {
@@ -218,7 +218,7 @@ export class ConfigGitSubProvider implements GitConfigSubProvider {
218
218
 
219
219
  const author = `${user.name} <${user.email}>`;
220
220
  // Check if there is a mailmap for the current user
221
- const result = await this.git.exec({ cwd: repoPath, errors: 'ignore' }, 'check-mailmap', author);
221
+ const result = await this.git.run({ cwd: repoPath, errors: 'ignore' }, 'check-mailmap', author);
222
222
 
223
223
  if (result.stdout && result.stdout !== author) {
224
224
  const match = mappedAuthorRegex.exec(result.stdout);
@@ -475,7 +475,7 @@ export class ConfigGitSubProvider implements GitConfigSubProvider {
475
475
  if (this.context.workspace?.isTrusted === false) {
476
476
  try {
477
477
  await fs.stat(joinPaths(cwd, 'HEAD'));
478
- result = await this.git.exec(
478
+ result = await this.git.run(
479
479
  { cwd: cwd, errors: 'throw', configs: ['-C', cwd] },
480
480
  'rev-parse',
481
481
  '--show-cdup',
@@ -490,7 +490,7 @@ export class ConfigGitSubProvider implements GitConfigSubProvider {
490
490
  }
491
491
 
492
492
  try {
493
- result = await this.git.exec(
493
+ result = await this.git.run(
494
494
  { cwd: cwd, errors: 'throw' },
495
495
  'rev-parse',
496
496
  '--show-toplevel',
@@ -551,7 +551,7 @@ export class ConfigGitSubProvider implements GitConfigSubProvider {
551
551
 
552
552
  const inDotGit = /this operation must be run in a work tree/.test(ex.stderr);
553
553
  if (inDotGit && this.context.workspace?.isTrusted !== false) {
554
- result = await this.git.exec({ cwd: cwd, errors: 'ignore' }, 'rev-parse', '--is-bare-repository');
554
+ result = await this.git.run({ cwd: cwd, errors: 'ignore' }, 'rev-parse', '--is-bare-repository');
555
555
  if (result.stdout.trim() === 'true') {
556
556
  const result = await this.revParseGitDir(cwd);
557
557
  const repoPath = result?.commonPath ?? result?.path;
@@ -578,12 +578,7 @@ export class ConfigGitSubProvider implements GitConfigSubProvider {
578
578
  }
579
579
 
580
580
  private async revParseGitDir(cwd: string): Promise<{ path: string; commonPath?: string } | undefined> {
581
- const result = await this.git.exec(
582
- { cwd: cwd, errors: 'ignore' },
583
- 'rev-parse',
584
- '--git-dir',
585
- '--git-common-dir',
586
- );
581
+ const result = await this.git.run({ cwd: cwd, errors: 'ignore' }, 'rev-parse', '--git-dir', '--git-common-dir');
587
582
  if (!result.stdout) return undefined;
588
583
 
589
584
  let [dotGitPath, commonDotGitPath] = result.stdout.split('\n').map(r => r.trimStart());
@@ -704,7 +699,7 @@ export class ConfigGitSubProvider implements GitConfigSubProvider {
704
699
  // Remove legacy keys from regular git config (use --unset-all defensively)
705
700
  for (const [key] of migrateConfig) {
706
701
  try {
707
- await this.git.exec(
702
+ await this.git.run(
708
703
  { cwd: repoPath, errors: 'ignore', runLocally: true },
709
704
  'config',
710
705
  '--local',
@@ -260,7 +260,7 @@ export class ContributorsGitSubProvider implements GitContributorsSubProvider {
260
260
  }
261
261
 
262
262
  const currentUserPromise = this.provider.config.getCurrentUser(repoPath).catch(() => undefined);
263
- const result = await this.git.exec({ cwd: repoPath, cancellation: signal }, ...args);
263
+ const result = await this.git.run({ cwd: repoPath, cancellation: signal }, ...args);
264
264
  if (!result.stdout) return [];
265
265
 
266
266
  const shortlog = parseShortlog(result.stdout, commonPath, await currentUserPromise);
@@ -301,7 +301,7 @@ export class ContributorsGitSubProvider implements GitContributorsSubProvider {
301
301
 
302
302
  const scope = getScopedLogger();
303
303
 
304
- const getCore = async (commonPath: string): Promise<GitContributorsStats | undefined> => {
304
+ const getCore = async (commonPath: string, signal?: AbortSignal): Promise<GitContributorsStats | undefined> => {
305
305
  try {
306
306
  const args = ['shortlog', '-s', '--all'];
307
307
 
@@ -316,10 +316,7 @@ export class ContributorsGitSubProvider implements GitContributorsSubProvider {
316
316
  args.push(`--since=${options.since}`);
317
317
  }
318
318
 
319
- const result = await this.git.exec(
320
- { cwd: commonPath, cancellation: cancellation, timeout: timeout },
321
- ...args,
322
- );
319
+ const result = await this.git.run({ cwd: commonPath, cancellation: signal, timeout: timeout }, ...args);
323
320
  if (!result.stdout) return undefined;
324
321
 
325
322
  const contributions = result.stdout
@@ -351,11 +348,9 @@ export class ContributorsGitSubProvider implements GitContributorsSubProvider {
351
348
  customCacheTTL = timeout * 2;
352
349
  }
353
350
 
354
- return this.cache.getContributorsStats(
355
- repoPath,
356
- cacheKey,
357
- getCore,
358
- customCacheTTL ? { accessTTL: customCacheTTL } : undefined,
359
- );
351
+ return this.cache.getContributorsStats(repoPath, cacheKey, getCore, {
352
+ accessTTL: customCacheTTL,
353
+ cancellation: cancellation,
354
+ });
360
355
  }
361
356
  }
@@ -60,7 +60,7 @@ export class DiffGitSubProvider implements GitDiffSubProvider {
60
60
  repoPath: string,
61
61
  to?: string,
62
62
  from?: string,
63
- options?: { uris?: (string | Uri)[] },
63
+ options?: { uris?: (string | Uri)[]; includeUntracked?: boolean },
64
64
  _cancellation?: AbortSignal,
65
65
  ): Promise<GitDiffShortStat | undefined> {
66
66
  const scope = getScopedLogger();
@@ -76,7 +76,7 @@ export class DiffGitSubProvider implements GitDiffSubProvider {
76
76
  }
77
77
 
78
78
  try {
79
- const result = await this.git.exec(
79
+ const result = await this.git.run(
80
80
  { cwd: repoPath, configs: gitConfigsDiff },
81
81
  'diff',
82
82
  '--shortstat',
@@ -85,9 +85,24 @@ export class DiffGitSubProvider implements GitDiffSubProvider {
85
85
  '--',
86
86
  ...(options?.uris?.map(p => this.provider.getRelativePath(p, repoPath)) ?? []),
87
87
  );
88
- if (!result.stdout) return undefined;
89
88
 
90
- return parseGitDiffShortStat(result.stdout);
89
+ const stat = result.stdout ? parseGitDiffShortStat(result.stdout) : undefined;
90
+
91
+ // Merge in untracked files when the comparison involves the working tree.
92
+ // Skip when a pathspec filter is active — mixing a path-filtered diff count with an
93
+ // unfiltered untracked count would silently produce wrong totals.
94
+ if (options?.includeUntracked && !options.uris?.length && isWorkingTreeComparison(to, from)) {
95
+ const untracked = (await this.provider.status?.getUntrackedFiles?.(repoPath)) ?? [];
96
+ if (untracked.length) {
97
+ return {
98
+ files: (stat?.files ?? 0) + untracked.length,
99
+ additions: stat?.additions ?? 0,
100
+ deletions: stat?.deletions ?? 0,
101
+ };
102
+ }
103
+ }
104
+
105
+ return stat;
91
106
  } catch (ex) {
92
107
  const msg: string = ex?.toString() ?? '';
93
108
  if (GitErrors.noMergeBase.test(msg)) {
@@ -134,7 +149,7 @@ export class DiffGitSubProvider implements GitDiffSubProvider {
134
149
 
135
150
  let result;
136
151
  try {
137
- result = await this.git.exec(
152
+ result = await this.git.run(
138
153
  { cwd: repoPath, configs: gitConfigsDiff, errors: 'throw', env: options?.index?.env },
139
154
  'diff',
140
155
  ...args,
@@ -173,7 +188,7 @@ export class DiffGitSubProvider implements GitDiffSubProvider {
173
188
  contents: string,
174
189
  _cancellation?: AbortSignal,
175
190
  ): Promise<GitDiffFiles | undefined> {
176
- const result = await this.git.exec(
191
+ const result = await this.git.run(
177
192
  { cwd: repoPath, configs: gitConfigsLog, stdin: contents },
178
193
  'apply',
179
194
  '--numstat',
@@ -194,7 +209,13 @@ export class DiffGitSubProvider implements GitDiffSubProvider {
194
209
  repoPath: string,
195
210
  ref1OrRange: string | GitRevisionRange,
196
211
  ref2?: string,
197
- options?: { filters?: GitDiffFilter[]; path?: string; renameLimit?: number; similarityThreshold?: number },
212
+ options?: {
213
+ filters?: GitDiffFilter[];
214
+ includeUntracked?: boolean;
215
+ path?: string;
216
+ renameLimit?: number;
217
+ similarityThreshold?: number;
218
+ },
198
219
  ): Promise<GitFile[] | undefined> {
199
220
  try {
200
221
  const similarityThreshold = options?.similarityThreshold;
@@ -202,7 +223,7 @@ export class DiffGitSubProvider implements GitDiffSubProvider {
202
223
  options?.renameLimit != null
203
224
  ? [...gitConfigsDiff, '-c', `diff.renameLimit=${options.renameLimit}`]
204
225
  : gitConfigsDiff;
205
- const result = await this.git.exec(
226
+ const result = await this.git.run(
206
227
  { cwd: repoPath, configs: configs },
207
228
  'diff',
208
229
  '--numstat',
@@ -216,9 +237,33 @@ export class DiffGitSubProvider implements GitDiffSubProvider {
216
237
  '--',
217
238
  options?.path ? options.path : undefined,
218
239
  );
219
- if (!result.stdout) return undefined;
220
240
 
221
- return parseGitDiffNumStatFiles(result.stdout, repoPath);
241
+ const files = result.stdout ? (parseGitDiffNumStatFiles(result.stdout, repoPath) ?? []) : [];
242
+
243
+ // Merge in untracked files when the comparison involves the working tree.
244
+ // Skip when `filters` excludes additions (untracked entries are semantically 'A'/added),
245
+ // or when `path` is a non-exact pathspec we can't reliably match against the untracked list.
246
+ if (
247
+ options?.includeUntracked &&
248
+ ref2 == null &&
249
+ typeof ref1OrRange === 'string' &&
250
+ !isRevisionRange(ref1OrRange) &&
251
+ !isUncommittedStaged(ref1OrRange) &&
252
+ filtersAllowUntracked(options.filters) &&
253
+ !isNonExactPathspec(options.path)
254
+ ) {
255
+ const untracked = (await this.provider.status?.getUntrackedFiles?.(repoPath)) ?? [];
256
+ if (untracked.length) {
257
+ const seen = new Set(files.map(f => f.path));
258
+ for (const file of untracked) {
259
+ if (seen.has(file.path)) continue;
260
+ if (options.path && options.path !== file.path) continue;
261
+ files.push(file);
262
+ }
263
+ }
264
+ }
265
+
266
+ return files.length ? files : undefined;
222
267
  } catch (_ex) {
223
268
  return undefined;
224
269
  }
@@ -283,7 +328,7 @@ export class DiffGitSubProvider implements GitDiffSubProvider {
283
328
  // Follow file history and specify the file path
284
329
  args.push('--follow', '--', relativePath);
285
330
 
286
- const result = await this.git.exec({ cwd: repoPath, configs: gitConfigsLog }, ...args);
331
+ const result = await this.git.run({ cwd: repoPath, configs: gitConfigsLog }, ...args);
287
332
 
288
333
  let currentSha;
289
334
  let currentPath;
@@ -511,7 +556,7 @@ export class DiffGitSubProvider implements GitDiffSubProvider {
511
556
 
512
557
  args.push('--follow', rev!, '--', relativePath);
513
558
 
514
- const result = await this.git.exec(
559
+ const result = await this.git.run(
515
560
  {
516
561
  cwd: repoPath,
517
562
  configs: gitConfigsLog,
@@ -636,7 +681,7 @@ export class DiffGitSubProvider implements GitDiffSubProvider {
636
681
 
637
682
  let result: GitResult;
638
683
  try {
639
- result = await this.git.exec({ cwd: repoPath, configs: gitConfigsLog }, ...args);
684
+ result = await this.git.run({ cwd: repoPath, configs: gitConfigsLog }, ...args);
640
685
  } catch (ex) {
641
686
  if (rev && !isUncommittedStaged(rev)) throw ex;
642
687
 
@@ -659,7 +704,7 @@ export class DiffGitSubProvider implements GitDiffSubProvider {
659
704
  };
660
705
 
661
706
  args.splice(index, 1, `-L${range.startLine},${range.endLine}:${relativePath}`);
662
- result = await this.git.exec({ cwd: repoPath, configs: gitConfigsLog }, ...args);
707
+ result = await this.git.run({ cwd: repoPath, configs: gitConfigsLog }, ...args);
663
708
  }
664
709
 
665
710
  let currentRange;
@@ -761,7 +806,7 @@ export class DiffGitSubProvider implements GitDiffSubProvider {
761
806
  }
762
807
 
763
808
  try {
764
- return await this.git.exec(
809
+ return await this.git.run(
765
810
  { cwd: repoPath, configs: gitConfigsDiff, encoding: options?.encoding },
766
811
  ...params,
767
812
  '--',
@@ -805,7 +850,7 @@ export class DiffGitSubProvider implements GitDiffSubProvider {
805
850
  params.push('--no-index');
806
851
 
807
852
  try {
808
- const result = await this.git.exec(
853
+ const result = await this.git.run(
809
854
  {
810
855
  cwd: repoPath,
811
856
  configs: gitConfigsDiff,
@@ -939,7 +984,7 @@ export class DiffGitSubProvider implements GitDiffSubProvider {
939
984
  scope?.debug(`Using tool=${tool}`);
940
985
  }
941
986
 
942
- await this.git.exec(
987
+ await this.git.run(
943
988
  { cwd: root },
944
989
  'difftool',
945
990
  '--no-prompt',
@@ -978,7 +1023,7 @@ export class DiffGitSubProvider implements GitDiffSubProvider {
978
1023
  scope?.debug(`Using tool=${tool}`);
979
1024
  }
980
1025
 
981
- await this.git.exec({ cwd: repoPath }, 'difftool', '--dir-diff', `--tool=${tool}`, ref1, ref2);
1026
+ await this.git.run({ cwd: repoPath }, 'difftool', '--dir-diff', `--tool=${tool}`, ref1, ref2);
982
1027
  } catch (ex) {
983
1028
  const msg: string = ex?.toString() ?? '';
984
1029
  if (msg === 'No diff tool found' || /Unknown .+? tool/.test(msg)) {
@@ -993,6 +1038,42 @@ export class DiffGitSubProvider implements GitDiffSubProvider {
993
1038
  }
994
1039
  }
995
1040
  }
1041
+ // Returns true when no --diff-filter is active or when it includes additions ('A' or '*').
1042
+ // Untracked files are semantically "added" — if a caller restricts filters to e.g. ['M'],
1043
+ // honor that and omit untracked.
1044
+ function filtersAllowUntracked(filters: GitDiffFilter[] | undefined): boolean {
1045
+ if (!filters?.length) return true;
1046
+ return filters.includes('A') || filters.includes('*');
1047
+ }
1048
+
1049
+ // Returns true when the path is a git pathspec we can't match with simple equality
1050
+ // (directory prefix, glob, or magic pathspec). In that case, skip the untracked merge
1051
+ // rather than risk excluding legitimate matches or including wrong ones.
1052
+ function isNonExactPathspec(path: string | undefined): boolean {
1053
+ if (!path) return false;
1054
+ return path.endsWith('/') || /[*?[\]]|^:/.test(path);
1055
+ }
1056
+
1057
+ // Returns true when the `git diff` invocation shape includes the working tree
1058
+ // (i.e. `git diff`, `git diff HEAD`, or `git diff <ref>` with no `from`), and
1059
+ // therefore untracked files are a meaningful addition to the result.
1060
+ function isWorkingTreeComparison(to: string | undefined, from: string | undefined): boolean {
1061
+ if (to == null) return true;
1062
+ if (isRevisionRange(to) || isUncommittedStaged(to)) return false;
1063
+
1064
+ // `to === uncommitted` always targets the working tree (`prepareToFromDiffArgs` sets
1065
+ // `from = 'HEAD'` when omitted, or diffs against the caller-supplied `from`).
1066
+ if (isUncommitted(to)) return true;
1067
+
1068
+ // `git diff <from>` — `to === ''` means the caller explicitly asked for working tree vs `from`
1069
+ if (to === '') return from == null || !isUncommittedStaged(from);
1070
+
1071
+ // `getChangedFilesCount` uses `prepareToFromDiffArgs`, which translates `to` (with no `from`)
1072
+ // to `${to}^ ${to}` UNLESS `to` is HEAD or empty.
1073
+ if (to.toUpperCase() === 'HEAD' && from == null) return true;
1074
+ return false;
1075
+ }
1076
+
996
1077
  function prepareToFromDiffArgs(
997
1078
  to: string,
998
1079
  from: string | undefined,
@@ -1063,7 +1144,7 @@ export async function findPathStatusChanged(
1063
1144
 
1064
1145
  const ordering = options?.ordering;
1065
1146
 
1066
- const result = await git.exec(
1147
+ const result = await git.run(
1067
1148
  { cwd: repoPath, configs: gitConfigsLog },
1068
1149
  'log',
1069
1150
  ...parser.arguments,