@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
@@ -12,6 +12,8 @@ export interface BranchContributionsOverview extends GitCommitStats<number> {
12
12
  readonly branch: string;
13
13
  readonly mergeTarget: string;
14
14
  readonly mergeBase: string;
15
+ /** Committer date of the merge-base commit. Lets consumers (e.g. the graph minimap scope window) anchor on the same date semantics as the minimap aggregation without a separate commit fetch. */
16
+ readonly mergeBaseDate: Date | undefined;
15
17
 
16
18
  readonly commits: number;
17
19
  readonly latestCommitDate: Date | undefined;
@@ -98,6 +100,7 @@ export interface GitBranchesSubProvider {
98
100
  getStoredUserMergeTargetBranchName?(repoPath: string, ref: string): Promise<string | undefined>;
99
101
  onCurrentBranchAccessed?(repoPath: string): Promise<void>;
100
102
  onCurrentBranchModified?(repoPath: string): Promise<void>;
103
+ onCurrentBranchAgentActivity?(repoPath: string): Promise<void>;
101
104
  renameBranch?(repoPath: string, oldName: string, newName: string): Promise<void>;
102
105
  setUpstreamBranch?(repoPath: string, name: string, upstream: string | undefined): Promise<void>;
103
106
  setBranchDisposition?(
@@ -68,6 +68,7 @@ export interface IncomingActivityOptions extends GitLogOptionsBase {
68
68
  }
69
69
 
70
70
  export interface GitCommitReachability {
71
+ readonly partial?: boolean;
71
72
  readonly refs: (
72
73
  | { readonly refType: 'branch'; readonly name: string; readonly remote: boolean; readonly current?: boolean }
73
74
  | { readonly refType: 'tag'; readonly name: string; readonly current?: never }
@@ -77,6 +78,12 @@ export interface GitCommitReachability {
77
78
  export interface GitCommitsSubProvider {
78
79
  getCommit(repoPath: string, rev: string, cancellation?: AbortSignal): Promise<GitCommit | undefined>;
79
80
  getCommitCount(repoPath: string, rev: string, cancellation?: AbortSignal): Promise<number | undefined>;
81
+ /** Cheap author/committer date lookup for a single revision. Skips the full commit parse (no files, parents, or message) — use when only the dates are needed. Pass a full SHA so caching stays correct (commits are immutable; refs are not). */
82
+ getCommitDates?(
83
+ repoPath: string,
84
+ rev: string,
85
+ cancellation?: AbortSignal,
86
+ ): Promise<{ authorDate: Date; committerDate: Date } | undefined>;
80
87
  getCommitFiles(repoPath: string, rev: string, cancellation?: AbortSignal): Promise<GitFileChange[]>;
81
88
  getCommitForFile(
82
89
  repoPath: string,
@@ -36,6 +36,8 @@ export type GkConfigKeys =
36
36
  | `branch.${string}.gk-last-accessed`
37
37
  /** `gk-last-modified` — ISO 8601 timestamp of when the branch last received a commit */
38
38
  | `branch.${string}.gk-last-modified`
39
+ /** `gk-agent-last-activity` — ISO 8601 timestamp of when an AI agent was last active on this branch */
40
+ | `branch.${string}.gk-agent-last-activity`
39
41
  /** `gk-disposition` — user-assigned branch disposition: 'starred' or 'archived' */
40
42
  | `branch.${string}.gk-disposition`
41
43
  /** `gk.defaultRemote` — the user-designated default remote for the repository */
@@ -32,7 +32,7 @@ export interface GitDiffSubProvider {
32
32
  repoPath: string,
33
33
  to?: string,
34
34
  from?: string,
35
- options?: { uris?: (string | Uri)[] },
35
+ options?: { uris?: (string | Uri)[]; includeUntracked?: boolean },
36
36
  cancellation?: AbortSignal,
37
37
  ): Promise<GitDiffShortStat | undefined>;
38
38
  getDiff?(
@@ -70,7 +70,13 @@ export interface GitDiffSubProvider {
70
70
  repoPath: string,
71
71
  ref1OrRange: string | GitRevisionRange,
72
72
  ref2?: string,
73
- options?: { filters?: GitDiffFilter[]; path?: string; renameLimit?: number; similarityThreshold?: number },
73
+ options?: {
74
+ filters?: GitDiffFilter[];
75
+ includeUntracked?: boolean;
76
+ path?: string;
77
+ renameLimit?: number;
78
+ similarityThreshold?: number;
79
+ },
74
80
  ): Promise<GitFile[] | undefined>;
75
81
  getDiffTool?(repoPath?: string): Promise<string | undefined>;
76
82
  getNextComparisonUris(
@@ -16,7 +16,7 @@ export interface GitOperationsSubProvider {
16
16
  cherryPick(
17
17
  repoPath: string,
18
18
  revs: string[],
19
- options?: { edit?: boolean; noCommit?: boolean },
19
+ options?: { edit?: boolean; noCommit?: boolean; source?: unknown },
20
20
  ): Promise<GitOperationResult>;
21
21
  commit(
22
22
  repoPath: string,
@@ -28,6 +28,7 @@ export interface GitOperationsSubProvider {
28
28
  author?: string;
29
29
  date?: string;
30
30
  signoff?: boolean;
31
+ source?: unknown;
31
32
  },
32
33
  ): Promise<void>;
33
34
  fetch(
@@ -43,7 +44,7 @@ export interface GitOperationsSubProvider {
43
44
  merge(
44
45
  repoPath: string,
45
46
  ref: string,
46
- options?: { fastForward?: boolean | 'only'; noCommit?: boolean; squash?: boolean },
47
+ options?: { fastForward?: boolean | 'only'; noCommit?: boolean; squash?: boolean; source?: unknown },
47
48
  ): Promise<GitOperationResult>;
48
49
  pull(
49
50
  repoPath: string,
@@ -51,6 +52,7 @@ export interface GitOperationsSubProvider {
51
52
  branch?: GitBranchReference | undefined;
52
53
  rebase?: boolean | undefined;
53
54
  tags?: boolean | undefined;
55
+ source?: unknown;
54
56
  },
55
57
  ): Promise<void>;
56
58
  push(
@@ -71,6 +73,7 @@ export interface GitOperationsSubProvider {
71
73
  interactive?: boolean;
72
74
  onto?: string;
73
75
  updateRefs?: boolean;
76
+ source?: unknown;
74
77
  },
75
78
  ): Promise<GitOperationResult>;
76
79
  reset(
@@ -78,5 +81,9 @@ export interface GitOperationsSubProvider {
78
81
  rev: string,
79
82
  options?: { mode?: 'hard' | 'keep' | 'merge' | 'mixed' | 'soft' },
80
83
  ): Promise<void>;
81
- revert(repoPath: string, refs: string[], options?: { editMessage?: boolean }): Promise<GitOperationResult>;
84
+ revert(
85
+ repoPath: string,
86
+ refs: string[],
87
+ options?: { editMessage?: boolean; source?: unknown },
88
+ ): Promise<GitOperationResult>;
82
89
  }
@@ -17,7 +17,11 @@ export interface ResolvedRevision {
17
17
  }
18
18
 
19
19
  export interface GitRevisionSubProvider {
20
- exists?(repoPath: string, path: string, revOrOptions?: string | { untracked?: boolean }): Promise<boolean>;
20
+ exists?(
21
+ repoPath: string,
22
+ path: string,
23
+ revOrOptions?: string | { untracked?: 'only' | 'include' },
24
+ ): Promise<boolean>;
21
25
  getRevisionContent(repoPath: string, path: string, rev: string): Promise<Uint8Array | undefined>;
22
26
  getSubmoduleHead?(repoPath: string, submodulePath: string): Promise<string | undefined>;
23
27
  /** Gets tracked file paths from the index (reflects working tree state, even during rebase) */
@@ -19,4 +19,6 @@ export interface GitStagingSubProvider {
19
19
  unstageFile(repoPath: string, pathOrUri: string | Uri): Promise<void>;
20
20
  unstageFiles(repoPath: string, pathsOrUris: (string | Uri)[]): Promise<void>;
21
21
  unstageDirectory(repoPath: string, directoryOrUri: string | Uri): Promise<void>;
22
+ stageAll(repoPath: string): Promise<void>;
23
+ unstageAll(repoPath: string): Promise<void>;
22
24
  }
@@ -1,9 +1,11 @@
1
1
  import { sortCompare } from '../../utils/string.js';
2
+ import { compareByVersionDescending } from '../../utils/version.js';
2
3
  import type { GitBranch } from '../models/branch.js';
3
4
  import type { GitContributor } from '../models/contributor.js';
4
5
  import type { GitRemote } from '../models/remote.js';
5
6
  import type { GitTag } from '../models/tag.js';
6
7
  import type { GitWorktree } from '../models/worktree.js';
8
+ import type { GitCommitReachability } from '../providers/commits.js';
7
9
 
8
10
  export type BranchSorting = 'date:desc' | 'date:asc' | 'name:asc' | 'name:desc';
9
11
  export type TagSorting = 'date:desc' | 'date:asc' | 'name:asc' | 'name:desc';
@@ -25,6 +27,25 @@ export interface BranchSortOptions {
25
27
  openedWorktreesByBranch?: Set<string>;
26
28
  }
27
29
 
30
+ /**
31
+ * Comparator for sorting reachable refs to match `git for-each-ref` ordering: current first,
32
+ * local before remote, tags by version descending. Use with `.sort()` or `.toSorted()`.
33
+ */
34
+ export function compareReachableRefs(
35
+ a: GitCommitReachability['refs'][number],
36
+ b: GitCommitReachability['refs'][number],
37
+ ): number {
38
+ if (a.current && !b.current) return -1;
39
+ if (!a.current && b.current) return 1;
40
+ if (a.refType === 'branch' && b.refType === 'branch') {
41
+ if (a.remote !== b.remote) return a.remote ? 1 : -1;
42
+ }
43
+ if (a.refType === 'tag' && b.refType === 'tag') {
44
+ return compareByVersionDescending(a.name, b.name);
45
+ }
46
+ return 0;
47
+ }
48
+
28
49
  export function sortBranches(branches: GitBranch[], options?: BranchSortOptions): GitBranch[] {
29
50
  options = { current: true, groupByType: true, orderBy: 'date:desc', ...options };
30
51
 
@@ -146,7 +146,7 @@ export class CliGitProvider implements GitProvider {
146
146
  folderPath = joinPaths(parentPath, `${remoteName}-${count}`);
147
147
  }
148
148
 
149
- await this._git.exec({ cwd: parentPath }, 'clone', url, folderPath);
149
+ await this._git.run({ cwd: parentPath }, 'clone', url, folderPath);
150
150
 
151
151
  return folderPath;
152
152
  }
@@ -17,6 +17,7 @@ import type {
17
17
  ResetErrorReason,
18
18
  RevertErrorReason,
19
19
  ShowErrorReason,
20
+ SigningErrorReason,
20
21
  StashApplyErrorReason,
21
22
  StashPushErrorReason,
22
23
  TagErrorReason,
@@ -24,6 +25,7 @@ import type {
24
25
  WorktreeDeleteErrorReason,
25
26
  } from '../../git/errors.js';
26
27
  import { WorkspaceUntrustedError } from '../../git/errors.js';
28
+ import type { SigningFormat } from '../../git/models/signature.js';
27
29
  import { CancellationError, getAbortSignalId, isCancellationError } from '../../utils/cancellation.js';
28
30
  import { getScopedCounter } from '../../utils/counter.js';
29
31
  import { getDurationMilliseconds, hrtime } from '../../utils/hrtime.js';
@@ -74,6 +76,8 @@ export const GitErrors = {
74
76
  detachedHead: /You are in 'detached HEAD' state/i,
75
77
  entryNotUpToDate: /error:\s*Entry ['"].+['"] not uptodate\. Cannot merge\./i,
76
78
  failedToDeleteDirectoryNotEmpty: /failed to delete '(.*?)': Directory not empty/i,
79
+ gpgNotFound: /gpg:?\s*(?:command\s+)?not found|'gpg' is not recognized as|cannot run gpg:/i,
80
+ gpgSignFailed: /^error:\s*gpg failed to sign the data/im,
77
81
  invalidName: /fatal:\s*'.+?' is not a valid branch name/i,
78
82
  invalidLineCount: /file .+? has only (\d+) lines/i,
79
83
  invalidObjectName: /invalid object name: (.*)\s/i,
@@ -106,6 +110,8 @@ export const GitErrors = {
106
110
  remoteAhead: /rejected because the remote contains work/i,
107
111
  remoteConnectionFailed: /Could not read from remote repository/i,
108
112
  remoteRejected: /rejected because the remote contains work/i,
113
+ signingKeyNotAvailable: /secret key not available|no secret key|no signing key/i,
114
+ sshNotFound: /ssh-keygen[^\n]*(?:not found|No such file or directory|is not recognized)/i,
109
115
  stashConflictingStagedAndUnstagedLines: /Cannot remove worktree changes/i,
110
116
  stashNothingToSave: /No local changes to save/i,
111
117
  stashSavedWorkingDirAndIndexState: /Saved working directory and index state/i,
@@ -413,6 +419,52 @@ export function getGitCommandError<T extends GitCommand, TReturn extends GitComm
413
419
  return creator(undefined);
414
420
  }
415
421
 
422
+ function extractSigningErrorText(ex: unknown): string {
423
+ if (ex == null) return '';
424
+ if (ex instanceof GitError) return ex.stderr || ex.stdout || ex.message || '';
425
+ if (ex instanceof Error) return ex.message || '';
426
+ if (typeof ex === 'string') return ex;
427
+ if (typeof ex === 'number' || typeof ex === 'boolean' || typeof ex === 'bigint') return String(ex);
428
+ if (typeof ex === 'object' && 'message' in ex && typeof ex.message === 'string') return ex.message;
429
+ return '';
430
+ }
431
+
432
+ /**
433
+ * Classifies a Git error as a signing-related failure using the {@link GitErrors}
434
+ * signing regexes. Returns the matched {@link SigningErrorReason}, or `undefined`
435
+ * when the error is not a recognized signing failure.
436
+ *
437
+ * Precedence is significant: `passphraseFailed` (gpg sign failure) is checked
438
+ * before `noKey`, matching the ordering used historically in patch.ts — a
439
+ * combined stderr like "error: gpg failed to sign … gpg: No secret key"
440
+ * resolves to `passphraseFailed`, the outer cause.
441
+ */
442
+ export function classifySigningError(ex: unknown): SigningErrorReason | undefined {
443
+ const text = extractSigningErrorText(ex);
444
+ if (!text) return undefined;
445
+ if (GitErrors.gpgSignFailed.test(text)) return 'passphraseFailed';
446
+ if (GitErrors.signingKeyNotAvailable.test(text)) return 'noKey';
447
+ if (GitErrors.gpgNotFound.test(text)) return 'gpgNotFound';
448
+ if (GitErrors.sshNotFound.test(text)) return 'sshNotFound';
449
+ return undefined;
450
+ }
451
+
452
+ /**
453
+ * Infers the {@link SigningFormat} from stderr hints (e.g., mentions of
454
+ * `ssh-keygen` or `gpg.ssh.*` config keys). Returns `undefined` when no hints
455
+ * are present so callers can supply their own default (typically `'gpg'`).
456
+ *
457
+ * Used as a fallback when `config.getSigningConfig` is unavailable or rejects
458
+ * on the error path.
459
+ */
460
+ export function inferSigningFormatFromError(ex: unknown): SigningFormat | undefined {
461
+ const text = extractSigningErrorText(ex);
462
+ if (!text) return undefined;
463
+ if (/\bssh-keygen\b|gpg\.ssh\.(?:program|allowedsignersfile)/i.test(text)) return 'ssh';
464
+ if (/\bgpg\b/i.test(text)) return 'gpg';
465
+ return undefined;
466
+ }
467
+
416
468
  export interface GitOptions {
417
469
  /** Custom environment variables to add to every git command */
418
470
  env?: Record<string, string | undefined>;
@@ -579,15 +631,15 @@ export class Git {
579
631
  );
580
632
  }
581
633
 
582
- async exec(
634
+ async run(
583
635
  options: ExitCodeOnlyGitCommandOptions,
584
636
  ...args: readonly (string | undefined)[]
585
637
  ): Promise<GitResult<unknown>>;
586
- async exec<T extends string | Buffer = string>(
638
+ async run<T extends string | Buffer = string>(
587
639
  options: GitExecOptions,
588
640
  ...args: readonly (string | undefined)[]
589
641
  ): Promise<GitResult<T>>;
590
- async exec<T extends string | Buffer = string>(
642
+ async run<T extends string | Buffer = string>(
591
643
  options: GitExecOptions,
592
644
  ...args: readonly (string | undefined)[]
593
645
  ): Promise<GitResult<T | unknown>> {
@@ -602,7 +654,7 @@ export class Git {
602
654
  options.caching.commonPath ?? options.cwd!,
603
655
  gitCommand,
604
656
  async cacheable => {
605
- const result = await this.execCore<T>({ ...options, caching: undefined }, runArgs, gitCommand);
657
+ const result = await this.runCore<T>({ ...options, caching: undefined }, runArgs, gitCommand);
606
658
  if (result.exitCode !== 0) {
607
659
  cacheable.invalidate();
608
660
  }
@@ -612,10 +664,10 @@ export class Git {
612
664
  );
613
665
  }
614
666
 
615
- return this.execCore<T>(options, runArgs, gitCommand);
667
+ return this.runCore<T>(options, runArgs, gitCommand);
616
668
  }
617
669
 
618
- private async execCore<T extends string | Buffer>(
670
+ private async runCore<T extends string | Buffer>(
619
671
  options: GitExecOptions,
620
672
  args: string[],
621
673
  gitCommand: string,
@@ -679,7 +731,7 @@ export class Git {
679
731
  // Execute through the queue (interactive/normal run immediately, background is throttled)
680
732
  const gitPath = await this.path();
681
733
  void this._queue
682
- .execute(priority, () => runSpawn<T>(gitPath, args, encoding ?? 'utf8', runOpts))
734
+ .run(priority, () => runSpawn<T>(gitPath, args, encoding ?? 'utf8', runOpts), cancellation)
683
735
  .then(deferred.fulfill, (e: unknown) => deferred.cancel(e instanceof Error ? e : new Error(String(e))))
684
736
  .finally(() => {
685
737
  this.pendingCommands.delete(cacheKey);
@@ -695,8 +747,8 @@ export class Git {
695
747
  try {
696
748
  result = await promise;
697
749
  return {
698
- stdout: result.stdout as T,
699
- stderr: result.stderr as T | undefined,
750
+ stdout: result.stdout,
751
+ stderr: result.stderr,
700
752
  exitCode: result.exitCode ?? 0,
701
753
  };
702
754
  } catch (ex) {
@@ -723,15 +775,15 @@ export class Git {
723
775
  if (errorHandling === 'ignore') {
724
776
  if (ex instanceof RunError) {
725
777
  return {
726
- stdout: ex.stdout as T,
727
- stderr: ex.stderr as T | undefined,
778
+ stdout: ex.stdout,
779
+ stderr: ex.stderr,
728
780
  exitCode: ex.code != null ? (typeof ex.code === 'number' ? ex.code : parseInt(ex.code, 10)) : 0,
729
781
  cancelled: ex instanceof CancelledRunError,
730
782
  };
731
783
  }
732
784
 
733
785
  return {
734
- stdout: '' as T,
786
+ stdout: '',
735
787
  stderr: undefined,
736
788
  exitCode: 0,
737
789
  cancelled: ex instanceof CancelledRunError,
@@ -747,7 +799,7 @@ export class Git {
747
799
 
748
800
  defaultExceptionHandler(exception, options.cwd, start);
749
801
  exception = undefined;
750
- return { stdout: '' as T, stderr: result?.stderr as T | undefined, exitCode: result?.exitCode ?? 0 };
802
+ return { stdout: '', stderr: result?.stderr, exitCode: result?.exitCode ?? 0 };
751
803
  } finally {
752
804
  this.logGitCommandComplete(gitCommand, exception, getDurationMilliseconds(start), waiting);
753
805
  }
@@ -888,7 +940,7 @@ export class Git {
888
940
  for await (const chunk of proc.stdout) {
889
941
  buffers.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
890
942
  }
891
- yield await decode(Buffer.concat(buffers as ReadonlyArray<Uint8Array>), { encoding: enc });
943
+ yield await decode(Buffer.concat(buffers), { encoding: enc });
892
944
  }
893
945
  }
894
946
  } finally {
@@ -914,7 +966,7 @@ export class Git {
914
966
  }
915
967
 
916
968
  async rev_parse__git_dir(cwd: string): Promise<{ path: string; commonPath?: string } | undefined> {
917
- const result = await this.exec({ cwd: cwd, errors: 'ignore' }, 'rev-parse', '--git-dir', '--git-common-dir');
969
+ const result = await this.run({ cwd: cwd, errors: 'ignore' }, 'rev-parse', '--git-dir', '--git-common-dir');
918
970
  if (!result.stdout) return undefined;
919
971
 
920
972
  // Keep trailing spaces which are part of the directory name
@@ -946,7 +998,7 @@ export class Git {
946
998
  // Check if the folder is a bare clone: if it has a file named HEAD && `rev-parse --show-cdup` is empty
947
999
  if (await fsExists(joinPaths(cwd, 'HEAD'))) {
948
1000
  try {
949
- result = await this.exec(
1001
+ result = await this.run(
950
1002
  { cwd: cwd, errors: 'throw', configs: ['-C', cwd] },
951
1003
  'rev-parse',
952
1004
  '--show-cdup',
@@ -962,7 +1014,7 @@ export class Git {
962
1014
  }
963
1015
 
964
1016
  try {
965
- result = await this.exec({ cwd: cwd, errors: 'throw' }, 'rev-parse', '--show-toplevel');
1017
+ result = await this.run({ cwd: cwd, errors: 'throw' }, 'rev-parse', '--show-toplevel');
966
1018
  // Make sure to normalize: https://github.com/git-for-windows/git/issues/2478
967
1019
  // Keep trailing spaces which are part of the directory name
968
1020
  return !result.stdout
@@ -985,7 +1037,7 @@ export class Git {
985
1037
  const inDotGit = GitWarnings.mustRunInWorkTree.test(ex.stderr ?? '');
986
1038
  // Check if we are in a bare clone
987
1039
  if (inDotGit && this.options.isTrusted?.() !== false) {
988
- result = await this.exec({ cwd: cwd, errors: 'ignore' }, 'rev-parse', '--is-bare-repository');
1040
+ result = await this.run({ cwd: cwd, errors: 'ignore' }, 'rev-parse', '--is-bare-repository');
989
1041
  if (result.stdout.trim() === 'true') {
990
1042
  const result = await this.rev_parse__git_dir(cwd);
991
1043
  const repoPath = result?.commonPath ?? result?.path;
@@ -1,11 +1,13 @@
1
1
  import type { GitCommandPriority } from './exec.types.js';
2
2
 
3
3
  interface QueuedCommand {
4
- execute: () => Promise<unknown>;
4
+ run: () => Promise<unknown>;
5
5
  resolve: (value: unknown) => void;
6
6
  reject: (reason: unknown) => void;
7
7
  queuedAt: number;
8
8
  priority: GitCommandPriority;
9
+ signal?: AbortSignal;
10
+ abortHandler?: () => void;
9
11
  }
10
12
 
11
13
  /** Priority levels in descending order (highest first) */
@@ -14,6 +16,10 @@ const priorities: GitCommandPriority[] = ['interactive', 'normal', 'background']
14
16
  /** How many extra slots interactive commands can use when at capacity */
15
17
  const interactiveBurstCapacity = 2;
16
18
 
19
+ function abortReason(signal: AbortSignal): Error {
20
+ return signal.reason instanceof Error ? signal.reason : new Error('Aborted');
21
+ }
22
+
17
23
  export interface GitQueueConfig {
18
24
  /** Maximum number of concurrent git processes. Defaults to 7. */
19
25
  maxConcurrent?: number;
@@ -67,6 +73,9 @@ export class GitQueue {
67
73
  this._disposed = true;
68
74
  for (const queue of this._queues.values()) {
69
75
  for (const cmd of queue) {
76
+ if (cmd.signal != null && cmd.abortHandler != null) {
77
+ cmd.signal.removeEventListener('abort', cmd.abortHandler);
78
+ }
70
79
  cmd.reject(new Error('GitQueue disposed'));
71
80
  }
72
81
  queue.length = 0;
@@ -89,7 +98,7 @@ export class GitQueue {
89
98
  }
90
99
 
91
100
  /** Check if a command at the given priority can execute immediately */
92
- private canExecuteNow(priority: GitCommandPriority): boolean {
101
+ private canRunNow(priority: GitCommandPriority): boolean {
93
102
  // Must yield to higher priority waiting
94
103
  if (this.hasHigherPriorityWaiting(priority)) return false;
95
104
 
@@ -115,20 +124,32 @@ export class GitQueue {
115
124
  return false;
116
125
  }
117
126
 
118
- /** Execute a git command with the specified priority */
119
- execute<T>(priority: GitCommandPriority, fn: () => Promise<T>): Promise<T> {
127
+ /**
128
+ * Execute a git command with the specified priority.
129
+ *
130
+ * When `signal` is provided:
131
+ * - If already aborted, rejects immediately without running.
132
+ * - If aborted while queued, the command is removed from the queue and rejected before it ever runs.
133
+ * - If aborted while running, the in-flight spawn must honor the same signal separately
134
+ * (callers pass it via `runOpts.cancellation`); the queue does not interrupt running work.
135
+ */
136
+ run<T>(priority: GitCommandPriority, fn: () => Promise<T>, signal?: AbortSignal): Promise<T> {
120
137
  if (this._disposed) {
121
138
  return Promise.reject(new Error('GitQueue disposed'));
122
139
  }
123
140
 
124
- if (!this.canExecuteNow(priority)) {
125
- return this.enqueue(fn, priority);
141
+ if (signal?.aborted) {
142
+ return Promise.reject(abortReason(signal));
126
143
  }
127
144
 
128
- return this.run(fn);
145
+ if (!this.canRunNow(priority)) {
146
+ return this.enqueue(fn, priority, signal);
147
+ }
148
+
149
+ return this.runcore(fn);
129
150
  }
130
151
 
131
- private async run<T>(fn: () => Promise<T>): Promise<T> {
152
+ private async runcore<T>(fn: () => Promise<T>): Promise<T> {
132
153
  this._activeCount++;
133
154
  try {
134
155
  return await fn();
@@ -138,7 +159,7 @@ export class GitQueue {
138
159
  }
139
160
  }
140
161
 
141
- private enqueue<T>(fn: () => Promise<T>, priority: GitCommandPriority): Promise<T> {
162
+ private enqueue<T>(fn: () => Promise<T>, priority: GitCommandPriority, signal?: AbortSignal): Promise<T> {
142
163
  const queue = this._queues.get(priority)!;
143
164
  const maxDepth = this._config.maxQueueDepth ?? 500;
144
165
  if (maxDepth > 0 && queue.length >= maxDepth) {
@@ -146,13 +167,29 @@ export class GitQueue {
146
167
  }
147
168
 
148
169
  return new Promise<T>((resolve, reject) => {
149
- queue.push({
150
- execute: fn as () => Promise<unknown>,
170
+ const cmd: QueuedCommand = {
171
+ run: fn,
151
172
  resolve: resolve as (value: unknown) => void,
152
173
  reject: reject,
153
174
  queuedAt: Date.now(),
154
175
  priority: priority,
155
- });
176
+ signal: signal,
177
+ };
178
+
179
+ if (signal != null) {
180
+ cmd.abortHandler = () => {
181
+ const idx = queue.indexOf(cmd);
182
+ // If still queued, remove and reject so it never runs.
183
+ // If already dequeued, the running command should honor the signal at its own layer.
184
+ if (idx !== -1) {
185
+ queue.splice(idx, 1);
186
+ cmd.reject(abortReason(signal));
187
+ }
188
+ };
189
+ signal.addEventListener('abort', cmd.abortHandler, { once: true });
190
+ }
191
+
192
+ queue.push(cmd);
156
193
  });
157
194
  }
158
195
 
@@ -172,6 +209,13 @@ export class GitQueue {
172
209
  }
173
210
 
174
211
  private runQueued(cmd: QueuedCommand): void {
212
+ // Detach the abort listener now that the command has left the queue — any subsequent
213
+ // abort must be handled by the running spawn itself (via runOpts.cancellation).
214
+ if (cmd.signal != null && cmd.abortHandler != null) {
215
+ cmd.signal.removeEventListener('abort', cmd.abortHandler);
216
+ cmd.abortHandler = undefined;
217
+ }
218
+
175
219
  const waitTime = Date.now() - cmd.queuedAt;
176
220
  if (waitTime > 1000) {
177
221
  this._hooks.onSlowQueue?.({
@@ -189,7 +233,7 @@ export class GitQueue {
189
233
 
190
234
  this._activeCount++;
191
235
  try {
192
- cmd.execute()
236
+ cmd.run()
193
237
  .then(cmd.resolve, cmd.reject)
194
238
  .finally(() => {
195
239
  this._activeCount--;
@@ -176,6 +176,10 @@ const stashMapping = {
176
176
  parents: '%P',
177
177
  stashName: '%gd',
178
178
  summary: '%gs',
179
+ // %s (commit subject) is git's authoritative `On <branch>: …` / `WIP on <branch>: …` record,
180
+ // present even when the reflog message (%gs) was customized (autostash, `git stash store -m`).
181
+ // Used downstream to extract `stashOnRef` reliably.
182
+ commitSubject: '%s',
179
183
  };
180
184
 
181
185
  type StashLogParser = LogParser<typeof stashMapping>;
@@ -882,7 +886,7 @@ function createLogParserWithPatch<T extends Record<string, string>>(
882
886
  }
883
887
 
884
888
  return {
885
- status: (fileMatch[1] === 'rename' ? 'R' : 'M') as GitFileIndexStatus,
889
+ status: (fileMatch[1] === 'rename' ? 'R' : 'M') satisfies GitFileIndexStatus,
886
890
  path: fileMatch[2],
887
891
  originalPath: fileMatch[1] === 'rename' ? fileMatch[3] : undefined,
888
892
  range: range,
@@ -393,7 +393,7 @@ export class BlameGitSubProvider implements GitBlameSubProvider {
393
393
  const { root, file, params, stdin } = await this.buildBlameArgs(repoPath, fileName, options);
394
394
 
395
395
  try {
396
- return await this.git.exec({ cwd: root, stdin: stdin }, ...params, '--', file);
396
+ return await this.git.run({ cwd: root, stdin: stdin }, ...params, '--', file);
397
397
  } catch (ex) {
398
398
  // Since `-c blame.ignoreRevsFile=` doesn't work (despite what the docs suggest),
399
399
  // detect the error and throw a more helpful one