@gitkraken/core-gitlens 0.7.2 → 0.9.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 (192) hide show
  1. package/CHANGELOG.md +72 -1
  2. package/dist/git/cache.d.ts +29 -5
  3. package/dist/git/cache.js +52 -11
  4. package/dist/git/cache.js.map +1 -1
  5. package/dist/git/context.d.ts +6 -1
  6. package/dist/git/context.js.map +1 -1
  7. package/dist/git/errors.d.ts +28 -1
  8. package/dist/git/errors.js +45 -1
  9. package/dist/git/errors.js.map +1 -1
  10. package/dist/git/features.d.ts +1 -1
  11. package/dist/git/features.js +1 -0
  12. package/dist/git/features.js.map +1 -1
  13. package/dist/git/models/reflog.d.ts +6 -0
  14. package/dist/git/models/reflog.js.map +1 -1
  15. package/dist/git/models/revision.d.ts +0 -1
  16. package/dist/git/models/revision.js +1 -2
  17. package/dist/git/models/revision.js.map +1 -1
  18. package/dist/git/providers/branches.d.ts +5 -1
  19. package/dist/git/providers/commits.d.ts +44 -2
  20. package/dist/git/providers/config.d.ts +7 -1
  21. package/dist/git/providers/operations.d.ts +19 -0
  22. package/dist/git/providers/provider.d.ts +29 -2
  23. package/dist/git/providers/refs.d.ts +48 -3
  24. package/dist/git/providers/revision.d.ts +7 -1
  25. package/dist/git/providers/staging.d.ts +13 -0
  26. package/dist/git/providers/stash.d.ts +3 -2
  27. package/dist/git/providers/status.d.ts +4 -0
  28. package/dist/git/providers/worktrees.d.ts +23 -3
  29. package/dist/git/run.types.d.ts +22 -0
  30. package/dist/git-cli/cliGitProvider.d.ts +39 -2
  31. package/dist/git-cli/cliGitProvider.js +109 -11
  32. package/dist/git-cli/cliGitProvider.js.map +1 -1
  33. package/dist/git-cli/exec/commandClassifier.d.ts +31 -0
  34. package/dist/git-cli/exec/commandClassifier.js +391 -0
  35. package/dist/git-cli/exec/commandClassifier.js.map +1 -0
  36. package/dist/git-cli/exec/git.d.ts +36 -5
  37. package/dist/git-cli/exec/git.js +163 -28
  38. package/dist/git-cli/exec/git.js.map +1 -1
  39. package/dist/git-cli/exec/gitQueue.d.ts +4 -1
  40. package/dist/git-cli/exec/gitQueue.js +1 -1
  41. package/dist/git-cli/exec/gitQueue.js.map +1 -1
  42. package/dist/git-cli/parsers/reflogParser.d.ts +7 -0
  43. package/dist/git-cli/parsers/reflogParser.js +11 -1
  44. package/dist/git-cli/parsers/reflogParser.js.map +1 -1
  45. package/dist/git-cli/providers/branches.d.ts +10 -1
  46. package/dist/git-cli/providers/branches.js +31 -22
  47. package/dist/git-cli/providers/branches.js.map +1 -1
  48. package/dist/git-cli/providers/commits.d.ts +27 -3
  49. package/dist/git-cli/providers/commits.js +112 -12
  50. package/dist/git-cli/providers/commits.js.map +1 -1
  51. package/dist/git-cli/providers/config.js +2 -1
  52. package/dist/git-cli/providers/config.js.map +1 -1
  53. package/dist/git-cli/providers/diff.js +4 -4
  54. package/dist/git-cli/providers/diff.js.map +1 -1
  55. package/dist/git-cli/providers/operations.d.ts +13 -0
  56. package/dist/git-cli/providers/operations.js +64 -5
  57. package/dist/git-cli/providers/operations.js.map +1 -1
  58. package/dist/git-cli/providers/patch.js +21 -45
  59. package/dist/git-cli/providers/patch.js.map +1 -1
  60. package/dist/git-cli/providers/refs.d.ts +20 -3
  61. package/dist/git-cli/providers/refs.js +140 -10
  62. package/dist/git-cli/providers/refs.js.map +1 -1
  63. package/dist/git-cli/providers/remotes.js +3 -2
  64. package/dist/git-cli/providers/remotes.js.map +1 -1
  65. package/dist/git-cli/providers/revision.d.ts +5 -1
  66. package/dist/git-cli/providers/revision.js +20 -5
  67. package/dist/git-cli/providers/revision.js.map +1 -1
  68. package/dist/git-cli/providers/staging.d.ts +9 -0
  69. package/dist/git-cli/providers/staging.js +63 -6
  70. package/dist/git-cli/providers/staging.js.map +1 -1
  71. package/dist/git-cli/providers/stash.d.ts +3 -2
  72. package/dist/git-cli/providers/stash.js +34 -22
  73. package/dist/git-cli/providers/stash.js.map +1 -1
  74. package/dist/git-cli/providers/status.d.ts +2 -0
  75. package/dist/git-cli/providers/status.js +27 -9
  76. package/dist/git-cli/providers/status.js.map +1 -1
  77. package/dist/git-cli/providers/worktrees.d.ts +31 -2
  78. package/dist/git-cli/providers/worktrees.js +75 -7
  79. package/dist/git-cli/providers/worktrees.js.map +1 -1
  80. package/dist/plus/git-github/api/github.d.ts +64 -3
  81. package/dist/plus/git-github/api/github.js +237 -24
  82. package/dist/plus/git-github/api/github.js.map +1 -1
  83. package/dist/plus/git-github/providers/github/branches.d.ts +3 -1
  84. package/dist/plus/git-github/providers/github/branches.js +2 -1
  85. package/dist/plus/git-github/providers/github/branches.js.map +1 -1
  86. package/dist/plus/git-github/providers/github/commits.d.ts +4 -2
  87. package/dist/plus/git-github/providers/github/commits.js +3 -1
  88. package/dist/plus/git-github/providers/github/commits.js.map +1 -1
  89. package/dist/plus/git-github/providers/github/refs.d.ts +10 -3
  90. package/dist/plus/git-github/providers/github/refs.js +4 -4
  91. package/dist/plus/git-github/providers/github/refs.js.map +1 -1
  92. package/dist/plus/git-github/providers/github/revision.d.ts +4 -1
  93. package/dist/plus/git-github/providers/github/revision.js +6 -1
  94. package/dist/plus/git-github/providers/github/revision.js.map +1 -1
  95. package/dist/plus/integrations/index.d.ts +5 -2
  96. package/dist/plus/integrations/index.js.map +1 -1
  97. package/dist/plus/integrations/integrationService.d.ts +35 -9
  98. package/dist/plus/integrations/integrationService.js +16 -3
  99. package/dist/plus/integrations/integrationService.js.map +1 -1
  100. package/dist/plus/integrations/manager.d.ts +107 -37
  101. package/dist/plus/integrations/models/gitHostIntegration.d.ts +128 -13
  102. package/dist/plus/integrations/models/gitHostIntegration.js +130 -17
  103. package/dist/plus/integrations/models/gitHostIntegration.js.map +1 -1
  104. package/dist/plus/integrations/models/integration.d.ts +51 -3
  105. package/dist/plus/integrations/models/integration.js +125 -18
  106. package/dist/plus/integrations/models/integration.js.map +1 -1
  107. package/dist/plus/integrations/models/issuesIntegration.d.ts +18 -5
  108. package/dist/plus/integrations/models/issuesIntegration.js +40 -10
  109. package/dist/plus/integrations/models/issuesIntegration.js.map +1 -1
  110. package/dist/plus/integrations/providers/azure/azure.d.ts +1 -1
  111. package/dist/plus/integrations/providers/azure/azure.js +13 -17
  112. package/dist/plus/integrations/providers/azure/azure.js.map +1 -1
  113. package/dist/plus/integrations/providers/azure/models.d.ts +29 -0
  114. package/dist/plus/integrations/providers/azure/models.js +112 -1
  115. package/dist/plus/integrations/providers/azure/models.js.map +1 -1
  116. package/dist/plus/integrations/providers/azureDevOps.d.ts +54 -2
  117. package/dist/plus/integrations/providers/azureDevOps.js +80 -2
  118. package/dist/plus/integrations/providers/azureDevOps.js.map +1 -1
  119. package/dist/plus/integrations/providers/azureDevOpsServer.js +2 -1
  120. package/dist/plus/integrations/providers/azureDevOpsServer.js.map +1 -1
  121. package/dist/plus/integrations/providers/bitbucket/bitbucket.d.ts +61 -0
  122. package/dist/plus/integrations/providers/bitbucket/bitbucket.js +165 -23
  123. package/dist/plus/integrations/providers/bitbucket/bitbucket.js.map +1 -1
  124. package/dist/plus/integrations/providers/bitbucket/models.d.ts +6 -1
  125. package/dist/plus/integrations/providers/bitbucket/models.js +4 -3
  126. package/dist/plus/integrations/providers/bitbucket/models.js.map +1 -1
  127. package/dist/plus/integrations/providers/bitbucket-server/pullRequestSearch.js +1 -1
  128. package/dist/plus/integrations/providers/bitbucket-server/pullRequestSearch.js.map +1 -1
  129. package/dist/plus/integrations/providers/bitbucket-server.d.ts +38 -1
  130. package/dist/plus/integrations/providers/bitbucket-server.js +33 -1
  131. package/dist/plus/integrations/providers/bitbucket-server.js.map +1 -1
  132. package/dist/plus/integrations/providers/bitbucket.d.ts +36 -1
  133. package/dist/plus/integrations/providers/bitbucket.js +29 -1
  134. package/dist/plus/integrations/providers/bitbucket.js.map +1 -1
  135. package/dist/plus/integrations/providers/github.d.ts +48 -4
  136. package/dist/plus/integrations/providers/github.js +101 -6
  137. package/dist/plus/integrations/providers/github.js.map +1 -1
  138. package/dist/plus/integrations/providers/gitlab/gitlab.d.ts +35 -0
  139. package/dist/plus/integrations/providers/gitlab/gitlab.js +132 -15
  140. package/dist/plus/integrations/providers/gitlab/gitlab.js.map +1 -1
  141. package/dist/plus/integrations/providers/gitlab/models.d.ts +1 -0
  142. package/dist/plus/integrations/providers/gitlab/models.js +1 -1
  143. package/dist/plus/integrations/providers/gitlab/models.js.map +1 -1
  144. package/dist/plus/integrations/providers/gitlab.d.ts +48 -1
  145. package/dist/plus/integrations/providers/gitlab.js +79 -3
  146. package/dist/plus/integrations/providers/gitlab.js.map +1 -1
  147. package/dist/plus/integrations/providers/jira-server.d.ts +3 -3
  148. package/dist/plus/integrations/providers/jira-server.js +3 -3
  149. package/dist/plus/integrations/providers/jira-server.js.map +1 -1
  150. package/dist/plus/integrations/providers/models.d.ts +12 -1
  151. package/dist/plus/integrations/providers/models.js +11 -1
  152. package/dist/plus/integrations/providers/models.js.map +1 -1
  153. package/dist/plus/integrations/providers/providerErrors.d.ts +9 -1
  154. package/dist/plus/integrations/providers/providerErrors.js +10 -1
  155. package/dist/plus/integrations/providers/providerErrors.js.map +1 -1
  156. package/dist/plus/integrations/providers/providersApi.d.ts +59 -0
  157. package/dist/plus/integrations/providers/providersApi.js +136 -1
  158. package/dist/plus/integrations/providers/providersApi.js.map +1 -1
  159. package/dist/plus/integrations/providers/utils/providerPaging.d.ts +42 -0
  160. package/dist/plus/integrations/providers/utils/providerPaging.js +58 -1
  161. package/dist/plus/integrations/providers/utils/providerPaging.js.map +1 -1
  162. package/dist/plus/integrations/providers/utils.d.ts +1 -1
  163. package/dist/plus/integrations/reads/currentAccount.d.ts +35 -0
  164. package/dist/plus/integrations/reads/currentAccount.js +46 -0
  165. package/dist/plus/integrations/reads/currentAccount.js.map +1 -0
  166. package/dist/plus/integrations/reads/drains.d.ts +4 -1
  167. package/dist/plus/integrations/reads/drains.js +7 -3
  168. package/dist/plus/integrations/reads/drains.js.map +1 -1
  169. package/dist/plus/integrations/reads/issueBatch.d.ts +72 -12
  170. package/dist/plus/integrations/reads/issueBatch.js +193 -48
  171. package/dist/plus/integrations/reads/issueBatch.js.map +1 -1
  172. package/dist/plus/integrations/reads/pullRequestBatch.d.ts +57 -0
  173. package/dist/plus/integrations/reads/pullRequestBatch.js +80 -0
  174. package/dist/plus/integrations/reads/pullRequestBatch.js.map +1 -0
  175. package/dist/plus/integrations/reads/pullRequestBranches.d.ts +58 -0
  176. package/dist/plus/integrations/reads/pullRequestBranches.js +111 -0
  177. package/dist/plus/integrations/reads/pullRequestBranches.js.map +1 -0
  178. package/dist/plus/integrations/reads/pullRequests.js +3 -3
  179. package/dist/plus/integrations/reads/pullRequests.js.map +1 -1
  180. package/dist/plus/integrations/reads/sweeps.js +3 -3
  181. package/dist/plus/integrations/reads/sweeps.js.map +1 -1
  182. package/dist/plus/integrations/results.d.ts +5 -2
  183. package/dist/plus/integrations/results.js.map +1 -1
  184. package/dist/utils/env/node/exec.js +1 -1
  185. package/dist/utils/env/node/exec.js.map +1 -1
  186. package/docs/integrations.md +90 -36
  187. package/docs/kepler-git-write-api-parity.md +365 -0
  188. package/docs/kepler-read-api-parity.md +75 -28
  189. package/package.json +1 -1
  190. package/dist/plus/integrations/reads/trackerIssue.d.ts +0 -33
  191. package/dist/plus/integrations/reads/trackerIssue.js +0 -60
  192. package/dist/plus/integrations/reads/trackerIssue.js.map +0 -1
@@ -0,0 +1,365 @@
1
+ # Kepler git write-API parity
2
+
3
+ Decision record for the git WRITE-path gaps Kepler hit while routing every one of its git calls through
4
+ `@gitkraken/core-gitlens` (gitkraken/kepler, branch `kepler/migrate-raw-git-spawns`). It is the sibling of
5
+ [`kepler-read-api-parity.md`](./kepler-read-api-parity.md), which covers provider reads; this one covers the
6
+ `git` sub-providers and the raw-command escape hatch.
7
+
8
+ The shape of the problem: Kepler drives three libraries (`@gitkraken/sync-tools`, `conflict-tools`,
9
+ `compose-tools`) that take a `{ exec }` git port plus optional typed ops, and know nothing of core; the host
10
+ supplies a typed op only where a core call serves it exactly. Kepler also has its own worktree, branch and
11
+ sync services. After the migration every command runs through core's executor, so it already shares the queue, environment, binary
12
+ resolution and logging. What it could NOT do is use a typed sub-provider method for a set of commands core had
13
+ no method for, or had one whose contract did not fit. Each such site stayed on `provider.git.run` with a
14
+ comment naming the missing API. This document is the list of those APIs, what was added for each, and what was
15
+ deliberately left alone.
16
+
17
+ §1–§9 were the first round. §10–§19 are the second: the commands Kepler still ran raw once the first round
18
+ shipped, each because a typed method lacked one option or one behavior.
19
+
20
+ Verdict legend: **added** (a typed method now exists) · **extended** (an existing method gained the missing
21
+ option) · **deferred** (left on the raw escape hatch on purpose, with the reason).
22
+
23
+ ## 1. Compare-and-swap ref updates — added
24
+
25
+ `refs.updateReference` ran `git update-ref <ref> <sha>` and swallowed every failure (logged, resolved
26
+ `undefined`), and there was no delete form. `@gitkraken/sync-tools` (0.4.0) publishes, applies and discards a
27
+ sync result itself (`applyPublishedSync`, `discardPublishedSync`), and every ref move in that flow is a
28
+ compare-and-swap: the real branch advances only if nothing else moved it since the library last read it. The
29
+ library asks its host for that through a typed `updateRef(ref, sha, { previousSha, mustNotExist, remove })`
30
+ op, and git's own `update-ref <ref> <new> <old>` is what makes it atomic. A writer that cannot refuse is a
31
+ clobber, so without this a host could only serve the op through the raw escape hatch.
32
+
33
+ - `updateReference(repoPath, ref, sha, options?: { expected?: string | 'absent' }, cancellation?)` now passes
34
+ the old value to git when `expected` is a sha, and the empty old value when it is `'absent'` (create only if
35
+ the ref does not exist yet). It THROWS a typed `ReferenceUpdateError` — reasons `conflict` (the ref moved,
36
+ or exists when it must not), `invalidRef`, `invalidObject`, `notFound` — instead of swallowing.
37
+ - `deleteReference(repoPath, ref, options?: { expected?: string }, cancellation?)` runs
38
+ `update-ref -d --no-deref <ref> [<expected>]` with the same error — `--no-deref` so a symbolic ref is itself
39
+ deleted rather than the ref it points to — and refuses `HEAD` (`checkedOut`). `update-ref -d` has none of
40
+ `git branch -d`'s guards or cleanup, so for a local branch (`refs/heads/<name>`) `deleteReference` supplies
41
+ them: it refuses a branch checked out in any worktree (`checkedOut`, read from `git worktree list`, not the
42
+ cached list), and removes the `branch.<name>` config section and GitLens's per-branch metadata with the ref.
43
+ Only `git branch -d`'s merged check is left out, since a compare-and-swap caller has already decided.
44
+ - Both fire the cache and repository hooks a branch, remote-tracking, tag or `HEAD` move fires; a ref in another
45
+ namespace (`refs/kepler/...`) announces nothing, since nothing core caches can observe it and an `unknown`
46
+ change would make a host refresh everything.
47
+
48
+ The sync-tools op maps onto these one-to-one:
49
+
50
+ | `updateRef` option | core call |
51
+ | ------------------ | ---------------------------------------------------------------- |
52
+ | `previousSha` | `updateReference(repoPath, ref, sha, { expected: previousSha })` |
53
+ | `mustNotExist` | `updateReference(repoPath, ref, sha, { expected: 'absent' })` |
54
+ | `remove` | `deleteReference(repoPath, ref, { expected: previousSha })` |
55
+ | none | `updateReference(repoPath, ref, sha)` |
56
+
57
+ `'absent'` against an existing ref rejects as `conflict` and leaves the ref where it was. Deleting a ref that
58
+ is already gone rejects as `notFound` when `expected` is given (the caller named a value it expected to
59
+ remove), and resolves when it is not.
60
+
61
+ The library reads any rejection as the ref's answer, so the host must tell a cancellation apart from a
62
+ refusal. A cancelled call rejects with core's `CancellationError` (`name === 'CancellationError'`), never a
63
+ `ReferenceUpdateError`, and that includes a signal already aborted when the call is made; a host adapting to
64
+ a library that expects `AbortError` maps it by name. A cancel that lands while git is running says nothing
65
+ about whether the ref moved, so re-read it before acting on the outcome.
66
+
67
+ **Breaking:** a caller that relied on a failed `updateReference` resolving silently now gets a rejection. No
68
+ caller in this repository did.
69
+
70
+ ## 2. Who clears after a write, and telling core about a raw one — changed / added
71
+
72
+ A typed mutator announces its write through `hooks.cache.onReset`, and core used to clear nothing itself on
73
+ that path: each host's handler was expected to forward to `Cache.clearCaches`. Two things followed. A host
74
+ with no handler kept serving pre-write reads after every typed write. And `clearCaches` only soft-invalidates
75
+ a shared entry that is still in flight, so even a wired host could hand a caller that arrived after the write
76
+ a read that started before it; GitLens also dropped the executor's pending commands on every reset, Kepler did
77
+ not.
78
+
79
+ A consumer that mutates through `provider.git.run` (a rebase driven by a library, a `clean`, a ref bookkeeping
80
+ write) had nothing to call at all, and typed reads served the pre-mutation answer until a file watcher caught
81
+ up, or forever with no watcher. Kepler classified verbs itself and called `clearCaches` after each one. The
82
+ sharpest case: it fetches PR heads through a raw `fetch` (it needs a per-process credential environment the
83
+ typed fetch cannot carry), and a typed `refs.validateReference` right after it served the pre-fetch tip for up
84
+ to 60s. `clearCaches` fixed that read but fired none of the host's hooks, so nothing else in the host learned
85
+ the refs moved.
86
+
87
+ - The provider wraps `onReset` when it is constructed. On every reset it hard-evicts its own caches for those
88
+ types (`Cache.evictCaches`, so a later caller never joins pre-write work), drops the written repository's
89
+ pending commands (its registered worktrees' included, no other repository's), then calls the host's handler.
90
+ Clears driven by a file watcher reach `clearCaches` directly and keep sharing in-flight reads, which is what
91
+ stops a burst of file events from spawning duplicate reads. A host's handler now only has host-side state to
92
+ update; forwarding to `clearCaches` as well is redundant but harmless.
93
+ - `provider.notifyChanged(repoPath, changes: RepositoryChange[], options?: { cache?: CachedGitTypes[] | 'all' })`
94
+ fires the same `cache.onReset` and `repository.onChanged` a typed write would, so a raw mutation gets the
95
+ same treatment, host listeners included. Without `cache` it resets what `changes` map to, the same caches a
96
+ file watcher's change of those kinds clears; an empty `changes`, or one naming `unknown`, resets everything,
97
+ and `cache: 'all'` always does. A consumer naming its own changes must name every kind the write can touch
98
+ (a `commit` that concludes a merge is `pausedOp` too), or those caches keep their pre-write answer.
99
+
100
+ ## 3. Worktree administration — added / extended
101
+
102
+ - `worktrees.pruneWorktrees(repoPath, options?: { expire? })` — `git worktree prune`. Kepler prunes
103
+ after a directory it removed by hand, and before reusing a path git still lists. Like `deleteWorktree`, it
104
+ unregisters each worktree it removed from the cache.
105
+ - `worktrees.lockWorktree(repoPath, path, options?: { reason? })` — `git worktree lock --reason`. The reason
106
+ is how Kepler marks a worktree as task-owned; only `unlockWorktree` existed.
107
+ - `worktrees.createWorktree(repoPath, path, options?, runOptions?)` and
108
+ `deleteWorktree(repoPath, path, options?, runOptions?)` — gained run options. A large worktree's checkout or
109
+ removal legitimately outlasts the default per-command timeout, and a caller may need to cancel either. A
110
+ cancelled call rejects as `CancellationError`, not a worktree error. The thrown `WorktreeDeleteError` keeps git's
111
+ raw output reachable on `original`, which is what a platform-specific retry classifier needs.
112
+
113
+ ## 4. `git clean`, and the staging sub-provider's missing hooks — added / fixed
114
+
115
+ - `staging.clean(repoPath, options?: { paths?, directories?, force?, ignored? })` — the only way to
116
+ discard untracked files; `ops.restore` cannot.
117
+ - Every staging mutation (`stageFile(s)`, `stageDirectory`, `stageAll`, `unstage*`, `removeFile(s)`, `clean`)
118
+ now fires `onReset('status', 'diff', 'tracking')` and the index change, except when it targets a temporary
119
+ index, which is not the repository's. Before this, a consumer that staged a conflict resolution through the
120
+ typed API and then read `getConflictingFiles()` could be answered from the index as it stood before the
121
+ stage.
122
+
123
+ ## 5. Upstream and hooks config keys — added
124
+
125
+ Kepler writes `branch.<name>.remote` / `branch.<name>.merge` (to set an upstream whose remote ref does not
126
+ exist yet, which `--set-upstream-to` refuses) and reads `core.hooksPath`. All three are now documented members
127
+ of `GitConfigKeys`, so `config.getConfig` / `setConfig` serve them with the usual cache. Writing either
128
+ upstream key also resets the cached branch list, which reports the upstream.
129
+
130
+ ## 6. `git pull --ff-only` — extended
131
+
132
+ `ops.pull` gained `fastForward: 'only'`, spelled as `ops.merge` spells it. Kepler advances a local base branch
133
+ before forking a task worktree from it, and must not create a merge commit or start a rebase in a checkout it
134
+ does not own. A refused fast-forward is a `PullError` with the new `noFastForward` reason (the name
135
+ `FetchError` already uses); before this it fell through to `other`. A base branch checked out nowhere has no
136
+ working tree to pull into, so it is advanced by fetching `<upstream>:<branch>`, which git refuses unless it
137
+ fast-forwards; without the option that path only fetches. That fetch runs without `--update-head-ok`: "checked
138
+ out nowhere" comes from the cached worktree list, so git's own refusal is what keeps a branch that is in fact
139
+ checked out from moving. A branch with no upstream is `PullError` reason `noUpstream`, on this path and for a
140
+ checked-out pull alike. `merge`'s `true` / `false` are not offered: a pull configured to rebase ignores `--ff`
141
+ / `--no-ff`.
142
+
143
+ ## 7. A commit from a tree with several parents and an author — added
144
+
145
+ A compose or stack engine writes merge commits and replays commits with their original author, which the
146
+ single-parent, identity-less `commits.createUnreachableCommitFromTree` could not.
147
+
148
+ - `commits.createCommitFromTree(repoPath, tree, { parents, message, author?, committer?, sign?, source? })` runs
149
+ `commit-tree` with the message on stdin and the identity in the per-call environment, returning the sha.
150
+ With `sign` it reports a signing failure as `SigningError` and fires the signing hooks.
151
+ - It is the only path that writes a commit object. `createUnreachableCommitFromTree` is removed, and the patch
152
+ sub-provider's commits (`createUnreachableCommitForPatch`, `createUnreachableCommitsFromPatches`,
153
+ `createEmptyInitialCommit`) go through it with unchanged results.
154
+
155
+ ## 8. `git init` — added
156
+
157
+ `GitProvider.clone` existed; `init(path, options?: { defaultBranch?, bare? })` did not. Kepler creates a
158
+ repository for a folder the user adds.
159
+
160
+ ## 9. Forced ref reads — added
161
+
162
+ After a change core neither made nor was told about (another process moving a branch, a raw `fetch` the
163
+ consumer has not reported yet), an ordinary read can answer from cache until its TTL runs out. `notifyChanged`
164
+ is the fix when the consumer made the change; `force` is for a read that must be current regardless.
165
+
166
+ - `refs.validateReference(repoPath, ref, { relativePath?, force? }, cancellation?)`,
167
+ `refs.getReference(repoPath, ref, { force? }, cancellation?)`,
168
+ `branches.getBranch(repoPath, name?, { force? }, cancellation?)` and
169
+ `revision.resolveRevision(repoPath, ref, pathOrUri?, { force? })`. A forced read drops its cache entry before
170
+ reading, so it neither returns the cached answer nor joins a read already in flight (a fresh entry gets a
171
+ fresh cancellation aggregate, whose id is part of the executor's dedup key), and it stores the fresh answer
172
+ for later unforced reads. `getStatus({ force })` and `getPausedOperationStatus({ force })` are the precedent.
173
+ - `getBranch({ force })` evicts the whole branch cascade first: branch lists, ref tips, merge bases, commit
174
+ counts and resolved revisions, the set a branch move clears, since each can be derived from a ref that moved.
175
+ `getReference({ force })` forces its validation and branch lookup; tags are not forced.
176
+ - `validateReference`'s `relativePath` moved into the options object, and `getBranch` / `getReference` take
177
+ options before `cancellation`, matching `getStatus`.
178
+
179
+ ## 10. An inherited git environment — fixed
180
+
181
+ A host launched by git (a hook, a `rebase -x` step, an editor opened as `GIT_EDITOR`) inherits that
182
+ repository's `GIT_DIR`, `GIT_WORK_TREE`, `GIT_INDEX_FILE` and friends, and git lets them override the working
183
+ directory, so every command core ran targeted the launching repository. The base environment now drops
184
+ `GIT_DIR`, `GIT_WORK_TREE`, `GIT_INDEX_FILE`, `GIT_COMMON_DIR`, `GIT_OBJECT_DIRECTORY`,
185
+ `GIT_ALTERNATE_OBJECT_DIRECTORIES` and `GIT_NAMESPACE` from the inherited `process.env`, plus the state git
186
+ hands a child about its own command: `GIT_CONFIG_PARAMETERS` (its `-c` options), `GIT_EXEC_PATH`, `GIT_PREFIX`
187
+ and `GIT_REFLOG_ACTION`. None is worth keeping when set deliberately: a `GIT_DIR` would point every repository
188
+ the host opens at one. `GIT_AUTHOR_*` / `GIT_COMMITTER_*` are different, since a deliberate identity (direnv,
189
+ say) breaks nothing, so they are dropped only when git launched the host, which it always marks by exporting
190
+ `GIT_EXEC_PATH`: an editor opened for `commit --amend` inherits that commit's author and date. `GIT_CONFIG_COUNT` /
191
+ `GIT_CONFIG_KEY_<n>` / `GIT_CONFIG_VALUE_<n>` are kept: git never sets them itself, so inherited ones are the
192
+ user's deliberate configuration. Only the inherited environment is scrubbed; a host's `env`, `getEnvironment()`
193
+ and a per-call `env` can still set any of these, as temporary-index staging does with `GIT_INDEX_FILE`.
194
+
195
+ ## 11. Run options on stash and clean — extended
196
+
197
+ `stash.applyStash`, `stash.saveStash` and `staging.clean` take the trailing `runOptions` (`env`,
198
+ `cancellation`, `timeout`) every other long-running operation already did. No typed default changed: see the
199
+ timeout decision below.
200
+
201
+ ## 12. A raw run that announces its own change — added
202
+
203
+ `git.run({ notify })` calls the provider's `notifyChanged` once the run settles, succeeded or failed (a
204
+ command that fails partway may still have moved something). An explicit `RepositoryChange[]` announces exactly
205
+ that. `'infer'` classifies the argv: read-only and object-store-only commands announce nothing, a write
206
+ announces its verb's changes for the repository it ran in (the `-C` target when given). Run under `-C`, a
207
+ write also announces to the caller's own tree the kinds every worktree sharing that `.git` sees (`heads`,
208
+ `tags`, `stash`, `remotes`, `worktrees`, `config`), since the target may be a worktree the cache never
209
+ registered; its HEAD, index, paused operation and FETCH_HEAD are the target's alone. A write it can't
210
+ classify resets both trees in full. Unset, nothing is classified and the run takes the same path as before.
211
+
212
+ ## 13. Fetch with explicit refspecs, and without FETCH_HEAD — extended
213
+
214
+ - `ops.fetch(repoPath, { remote, refspecs, prune? })` passes the refspecs verbatim after the remote
215
+ (`+refs/heads/x:refs/remotes/o/x`, `upstream:local`). It never adds `-u`, so git keeps refusing to write
216
+ into a checked-out branch, which a caller fast-forwarding a branch checked out nowhere relies on. A refspec
217
+ whose destination is a local branch or tag also announces `heads` or `tags`. The type refuses `refspecs`
218
+ beside `branch`, `all` or `pull`.
219
+ - `preserveFetchHead` passes `--no-write-fetch-head`, on every form of fetch. A checkout has one
220
+ `FETCH_HEAD`, and a user's own `git pull` reads it back right after its fetch, so a background fetch landing
221
+ in between breaks that pull. Git older than 2.29 has no such flag; there the option is ignored rather than
222
+ failing the fetch.
223
+
224
+ ## 14. Clone into an exact folder — extended
225
+
226
+ `clone(url, parentPath, { folderName }, runOptions)`, on both core's `GitProvider` and GitLens's
227
+ `GlGitProvider`. `folderName` is used as given, with no numbering past a taken name, so git's own refusal of a
228
+ non-empty folder surfaces. Without it the folder is derived from the URL and numbered as before. `runOptions`
229
+ carries the clone's credentials, cancellation and timeout.
230
+
231
+ ## 15. Status without untracked detail or branch state — extended
232
+
233
+ `status.getStatus(repoPath, { untracked: 'no' | 'normal' | 'all', branch: false })`. `untracked` picks git's
234
+ `-u` mode (default `all`, as before). `branch: false` skips `--branch`, whose ahead/behind against the upstream
235
+ is the slow part on a large divergence; the result's `branch`, `sha`, `upstream` and `detached` are then unset
236
+ and mean nothing. A narrower read gets its own dedup key, so a full-status caller never joins it and loses
237
+ files. `GIT_OPTIONAL_LOCKS=0` was already set on every status read, so there is no lock option.
238
+
239
+ ## 16. A reflog read filtered by message — added
240
+
241
+ `refs.getReflogEntries(repoPath, ref, { grep? })` returns `{ sha, message }` per entry, newest first, where
242
+ `grep` is git's `--grep-reflog` pattern. It resolves `[]` when nothing matches or the ref has no reflog, and
243
+ rejects when the read fails, so a caller can tell "no answer" from "never read". It runs `reflog show <ref> --`,
244
+ not `reflog <ref>`: a branch named `delete`, `expire` or `exists` would otherwise run that subcommand, and one
245
+ named like a tracked file would fail as ambiguous. Core's own base-branch inference moved onto it, which
246
+ fixed both cases there.
247
+
248
+ ## 17. Commit counts that exclude other refs — extended
249
+
250
+ `commits.getCommitCount(repoPath, rev, { excluding: { branches?, remotes?, tags?, refs?, except? } },
251
+ cancellation?)` counts what `rev` has that other refs don't, such as what deleting a branch would orphan.
252
+ `branches` / `remotes` / `tags` exclude a whole namespace, `refs` adds revisions verbatim, and `except` takes
253
+ full ref names back out of an excluded namespace. Core rewrites each `except` entry to the short form and
254
+ places it right before its own `--branches` / `--remotes` / `--tags`, because git matches `--exclude` against
255
+ the short name and applies it only to the next pseudo-ref option. The spelling that reads naturally,
256
+ `--exclude=refs/heads/x --branches`, excludes nothing. The GitHub provider returns `undefined` for an excluding
257
+ count, which its API cannot answer. A count is cached until a branch, remote-tracking branch or tag changes,
258
+ so a `refs` entry outside those namespaces (a tool's own `refs/<tool>/…`) can move without refreshing it;
259
+ pass its SHA instead, which keys a new read.
260
+
261
+ `commits.getLogShas` takes the same `excluding`, listing the SHAs instead of counting them; an omitted `rev` is
262
+ anchored to `HEAD` (a bare `--not` lists nothing), and the GitHub provider returns no SHAs.
263
+
264
+ **Breaking:** `getCommitCount` takes `options` before `cancellation`.
265
+
266
+ ## 18. The empty tree in the repository's object format — added / fixed
267
+
268
+ The empty tree's id differs between SHA-1 and SHA-256 repositories, and core hardcoded the SHA-1 one as
269
+ `rootSha`, so every diff against a root commit's missing parent failed in a SHA-256 repository.
270
+ `revision.getEmptyTreeSha(repoPath)` computes it with `hash-object -t tree --stdin` (no `-w`; git knows the
271
+ empty tree without storing it), once per repository and shared across its worktrees. Every `rootSha` use now
272
+ goes through it, and `rootSha` is removed. GitHub repositories are always SHA-1.
273
+
274
+ **Breaking:** `rootSha` is removed from `@gitlens/git/models/revision`.
275
+
276
+ ## 19. A link's clone of a large repository — fixed (GitLens)
277
+
278
+ Not a Kepler gap, found alongside §14: GitLens's deep-link clone ran under `gitlens.advanced.git.timeout` and
279
+ died partway through a large repository. It now runs uncapped, from a cancellable progress notification.
280
+
281
+ ## Overlaps with existing APIs, and how each was settled
282
+
283
+ - **Refs containing a sha.** `branches.getBranchesWithCommits(repoPath, [sha], undefined, { all, mode })` and
284
+ `tags.getTagsWithCommit` already answer it, so `getRefTips` did not gain `--contains` / `--points-at`
285
+ filters. Kepler's one use (is a sha on any other local or remote branch) is `getBranchesWithCommits`.
286
+ - **Config keys.** Added to the documented union rather than served by a separate untyped
287
+ `getConfigValue` / `setConfigValue`, which would have been a second way to read every key.
288
+ - **Commit objects.** `createCommitFromTree` replaced `createUnreachableCommitFromTree` and the patch
289
+ sub-provider's own `commit-tree` calls, rather than sitting beside them.
290
+ - **Deleting refs.** `deleteReference` stays beside `branches.deleteLocalBranch` and `tags.deleteTag` on
291
+ purpose. `deleteLocalBranch` keeps `git branch -d`'s merged check, which the delete flow's force-retry relies
292
+ on, and takes several names; `deleteTag` reports a `TagError` the UI renders. `deleteReference` is the
293
+ compare-and-swap form for any namespace, and shares the branch cleanup with `deleteLocalBranch`.
294
+ - **Clearing caches.** `evictCaches` stays beside `clearCaches`: a write hard-evicts, a watcher clear keeps
295
+ sharing in-flight reads (see the decisions below).
296
+ - **Reflog reads.** `getReflogEntries` stays beside `commits.getIncomingActivity`, which walks `HEAD`'s reflog
297
+ for merges and pulls, pages, and aggregates; a message grep on it would change its paging. Core's private
298
+ reflog greps were folded into `getReflogEntries` rather than kept.
299
+ - **Dirty checks.** `getStatus({ untracked, branch: false })` stays beside `status.hasWorkingChanges`, which
300
+ answers yes/no with early exits; a caller counting files needs the list.
301
+ - **Unpushed commits.** `getCommitCount({ excluding })` stays beside `hasUnpublishedCommits` /
302
+ `filterUnpublishedShas`, which are early-exit probes against remotes alone and count nothing.
303
+
304
+ ## Deferred, deliberately
305
+
306
+ - **Index and object plumbing** (`read-tree`, `write-tree`, `update-index --index-info`, `mktree`,
307
+ `hash-object -w --stdin`, `cat-file -p`). The libraries drive these through `exec` with a scratch
308
+ `GIT_INDEX_FILE` and parse the results themselves; a typed surface would re-encode the same argv with more
309
+ room to drift. Revisit when a second consumer needs them.
310
+ - **Generic `for-each-ref` with a caller-supplied format.** The `RefRecord` shape is private on purpose.
311
+ - **A typed `worktree remove` for every consumer.** Kept the raw form where a consumer classifies platform
312
+ failures from stderr; §3's `original` makes the typed form usable for that too.
313
+
314
+ ## Bridging core into the tools libraries
315
+
316
+ The libraries take their own port and know nothing of core, so a host serving one through core reshapes what
317
+ core rejects with:
318
+
319
+ - **Cancellation.** Core rejects with `CancellationError` (`name === 'CancellationError'`); a library that
320
+ expects `AbortError` gets it mapped by name.
321
+ - **"Git answered no" versus a structural failure.** sync-tools tells them apart by whether `err.code` is a
322
+ number, and compose-tools reads `err.stderr`: the shape of Node's `execFile` error. Core's `GitError`
323
+ carries `stderr` but reports the exit as `exitCode`, which is a number or numeric string for a real exit and
324
+ an errno string (`'ENOENT'`) when git never started. A typed method's own error, such as
325
+ `ReferenceUpdateError`, keeps that `GitError` on `original`.
326
+
327
+ ## Decisions worth recording, because each closes off a plausible-looking alternative
328
+
329
+ - `updateReference` throws rather than gaining an `errors: 'throw'` option. A ref writer whose failures are
330
+ invisible by default is the bug, not a mode.
331
+ - `'absent'` rather than `expected: ''`. The empty old value is git's encoding of "must not exist"; a caller
332
+ spelling it as an empty string reads like a mistake, and an argv filter that drops empty strings would turn
333
+ a create into an unconditional overwrite.
334
+ - New config keys join `GitConfigKeys` rather than widening it to `string` or adding an untyped read. The
335
+ union's value is that every listed key is documented; an escape hatch would keep the autocomplete and lose
336
+ the guarantee.
337
+ - `deleteReference` gives a branch `git branch -d`'s guard and cleanup rather than refusing branches. A
338
+ compare-and-swap delete of a branch is legitimate (sync-tools' port contract exercises one), and refusing it
339
+ would push the caller back to a raw `update-ref -d` that orphans the same state.
340
+ - A write hard-evicts while a watcher clear keeps sharing in-flight reads, rather than one policy for both. A
341
+ write is a known point after which an older read is wrong; a watcher tick has no such ordering, and sharing
342
+ the in-flight read is what keeps a burst of file events from multiplying git processes.
343
+ - `notifyChanged` on the provider, and `git.run({ notify })` bound to it rather than a second invalidation
344
+ path. The executor has no cache and no hooks, so the provider binds itself to its `Git` and a `notify` run
345
+ calls the same `notifyChanged`. `'infer'` classifies the argv (read-only and object-store-only commands
346
+ announce nothing; a `-C` write also announces to the caller's tree what every worktree sees), for a
347
+ consumer whose raw runs are too varied to name the changes by hand.
348
+ - Timeouts stay the caller's. No typed operation's default was lifted to "no timeout" for this round, even
349
+ for fetch or clone: GitLens passes its `gitlens.advanced.git.timeout` setting as core's default, and an
350
+ uncapped background fetch would hang with nothing to stop it. A caller that knows an operation is long
351
+ passes `timeout: 0` with a cancellation, as GitLens's deep-link clone now does.
352
+ - `except` takes full ref names and core converts them, rather than exposing git's short-form `--exclude`. The
353
+ short form is what every caller gets wrong, silently.
354
+ - Staging hooks skip the temporary-index case rather than firing unconditionally. A scratch index is not the
355
+ repository's; announcing an index change for it would invalidate status for nothing.
356
+
357
+ **Kepler-side follow-up**, once a core release carries these: serve sync-tools' `updateRef` op through
358
+ `updateReference` / `deleteReference` (mapping in §1), delete the review ref with `deleteReference`, drop the
359
+ verb classifier in `core-git-port.ts` in favour of `git.run({ notify: 'infer' })`, and drop its interim
360
+ environment scrub (§10). Then move the second round's raw sites onto their typed forms: credential fetches onto
361
+ `fetch({ remote, refspecs, preserveFetchHead: true })` (§13), the clone onto `clone(..., { folderName }, { env,
362
+ timeout: 0 })` (§14), the porcelain count onto `getStatus({ untracked: 'normal', branch: false, force: true })`
363
+ (§15), the creation probe onto `getReflogEntries` (§16), the unpushed count onto `getCommitCount({ excluding })`
364
+ (§17, which also fixes its always-zero `--exclude`), and the object-format probe onto `getEmptyTreeSha` (§18).
365
+ Then retire the remaining `TODO(core-gitlens)` residuals.
@@ -182,34 +182,81 @@ which excludes unassigned issues. A provider that declares no filtered search ke
182
182
  See the note in `reads/broaden.ts` and
183
183
  [`integrations.md` §9](./integrations.md#9-per-provider-behavior-worth-designing-around).
184
184
 
185
- **Batch issue resolution (#5802).** `getIssuesBatch` resolves N `(owner, repo, number)` coordinates in one
186
- request, for the identity question a search cannot answer: "which issue does this branch name reference".
187
- It aliases the point read rather than a search, so there is no result ceiling and no ordering — and an
188
- absent slot is a PROVEN absence rather than "not found within a page budget", which is what lets a consumer
189
- CACHE a miss. A target whose chunk failed is not returned at all, so the two stay distinguishable; caching a
190
- failure as an absence is the bug that distinction prevents. GitHub/GHE only, matching `countIssues`.
191
-
192
- **Tracker issue resolution by key (#5810).** `getTrackerIssue` is the same identity read for an issue TRACKER,
193
- which `getIssuesBatch` cannot serve: its target is `(owner, repo, number)`, and a tracker issue is addressed by
194
- `(resourceId, ABC-123)` — no owner, no repo, and an identifier that is not a number. It answers in one request
195
- where the only published tracker surface (`listIssueTrackerIssuesPage`) needs a scoped page-walk that cannot
196
- prove absence without draining the whole scope. Same absence/failure contract as the batch read, and the same
197
- reason it matters: a tracker miss is the common outcome and was never cacheable.
198
-
199
- `resourceId` is required and trusted, so the read performs no resource discovery. Jira also requires
200
- `resourceUrl`, obtained alongside the resource ID, because the issue response's `self` is an API endpoint rather
201
- than a browser link; supplying both retains the one-request contract. Linear needs only the resource ID. Jira and
202
- Linear only; Trello refuses because its single-issue read can fall back to a capped board scan for a numeric
203
- identifier, where a "not found" result cannot be distinguished from a card beyond the cap.
204
-
205
- **Jira Data Center point reads (#5872).** The same read serves a self-hosted Jira instance once it carries the host:
206
- `getTrackerIssue` takes the `domain` its paged siblings already took, and `resourceId` is the host itself (the
207
- instance's single resource; one naming another host than the read resolves to is refused). It does NOT fall back to the primary connection the way the paged reads do — a
208
- self-managed tracker requires a `domain` or a `connectionId` with a configured host and is refused otherwise —
209
- because its proven absence is cacheable and two self-hosted instances routinely issue the same keys, so an answer
210
- from whichever host is primary would be cached under a key naming a different instance. The branch-association
211
- resolver (`getIssueFromGitConfigEntityIdentifier`) follows the same rule from the other side: its resolver receives
212
- the identifier's `domain`, and a Jira Data Center identifier without one is dropped rather than resolved.
185
+ **Batch issue resolution (#5802, #5810).** `getIssuesBatch` resolves N issues by identity in one call, for the
186
+ identity question a search cannot answer: "which issue does this branch name reference". It uses the point read
187
+ rather than a search, so there is no result ceiling and no ordering — and an absent slot is a PROVEN absence
188
+ rather than "not found within a page budget", which is what lets a consumer CACHE a miss. A target that failed —
189
+ its whole request, or just that target, e.g. in an org enforcing SAML SSO the token isn't authorized for — is not
190
+ returned at all, so the two stay distinguishable; caching a failure as an absence is the bug that distinction
191
+ prevents.
192
+
193
+ A target takes the form its provider addresses an issue by. GitHub/GHE take `(owner, repo, number)` coordinates,
194
+ aliased up to 25 per request. A tracker issue is addressed by `(resourceId, ABC-123)` — no owner, no repo, and an
195
+ identifier that is not a number — so Jira (Cloud and Data Center) and Linear take
196
+ `{ key, resourceId, resourceUrl?, identifier }` targets, each answered by the single-issue read the tracker already
197
+ implements. That replaces a scoped page-walk over `listIssueTrackerIssuesPage`, which cannot prove absence without
198
+ draining the whole scope, where a miss is the common outcome. A key asked of several resources is one call with one
199
+ target per resource. `resourceId` is trusted, so the read performs no resource discovery; Jira Cloud also requires
200
+ `resourceUrl`, because the issue response's `self` is an API endpoint rather than a browser link. Trello refuses:
201
+ its single-issue read can fall back to a capped board scan for a numeric identifier, where a "not found" cannot be
202
+ told from a card beyond the cap. Bitbucket and Bitbucket DC, which have no issues, refuse too.
203
+
204
+ GitLab and Azure DevOps take the same coordinates as GitHub, plus `project` on Azure DevOps, whose work items belong to
205
+ the project rather than to a repository, so `repo` is ignored there. They cost one request per target and replace a
206
+ scoped page-walk over `listIssuesPage`, which could not prove absence either. Neither host's existing single-issue
207
+ read could, so each has a strict one. A GitLab miss costs a second request, because provider-apis reports a reply
208
+ carrying only GraphQL errors the same way as a missing issue. An Azure DevOps miss is trusted only on Azure's own
209
+ not-found body: `WorkItemUnauthorizedAccessException` (TF401232, "does not exist, or you do not have permissions to
210
+ read it", so absent means not visible to this connection) or `ProjectDoesNotExistWithNameException`. An HTML 404, a
211
+ 410 or any other failure fails the target. Rows match the repository-scoped `listIssuesPage` rows, except that an
212
+ Azure DevOps row has no `project`.
213
+
214
+ A self-hosted Jira (Data Center, #5872) is served by the same tracker targets once the call carries the host:
215
+ `getIssuesBatch` takes the `domain` its paged siblings take, and a target's `resourceId` is the host itself (the
216
+ instance's single resource; a call with any target naming another host than the read resolves to is refused whole).
217
+ It does NOT fall back to the primary connection the way the paged reads do — a self-managed tracker requires a
218
+ `domain` or a `connectionId` with a configured host and the call is refused otherwise — because its proven absence
219
+ is cacheable and two self-hosted instances routinely issue the same keys, so an answer from whichever host is
220
+ primary would be cached under a key naming a different instance. The branch-association resolver
221
+ (`getIssueFromGitConfigEntityIdentifier`) follows the same rule from the other side: its resolver receives the
222
+ identifier's `domain`, and a Jira Data Center identifier without one is dropped rather than resolved.
223
+
224
+ **Batch pull request resolution.** `getPullRequestsBatch` is the identity read for pull requests: N
225
+ `(owner, repo, number)` coordinates, plus `project` on Azure DevOps, in any state. Before it the facade had only
226
+ list, search, count and sweep reads for pull requests, so a consumer found one by scanning its repository's most
227
+ recently updated pull requests per state and matching the URL — and an older merged pull request could be neither
228
+ found nor proven gone. Same absence/failure contract as `getIssuesBatch`. It serves every git host, Bitbucket and
229
+ Bitbucket DC included: GitHub/GHE alias up to 25 point reads into one document, while GitLab, Bitbucket, Bitbucket
230
+ DC and Azure DevOps have no batch form and cost one request per target. A GitLab miss costs a second request, because
231
+ provider-apis' GitLab read reports a reply that carries only GraphQL errors the same way as a missing merge
232
+ request, so its `null` alone does not prove absence; an Azure DevOps miss is trusted only when Azure's own error
233
+ body names the pull request, repository or project as not found, not on any other 404 or a 410. Rows take the
234
+ list reads' conversion on every host but Bitbucket Cloud, which has no single pull request read in provider-apis
235
+ and so answers from GitLens' own REST read instead, missing `commentsCount`, `isDraft` and the clone URLs. On
236
+ GitLab, a miss the confirming read then contradicts fails the target rather than answering with the confirming
237
+ read's own, differently-identified row. The read is uncached and does not go through
238
+ `IntegrationCacheProvider.getPullRequest`, whose key carries no connection, so the consumer's cache is the only
239
+ one holding the answer.
240
+
241
+ **Pull requests by branch.** `getPullRequestsForBranches` answers "which pull requests have this branch as their
242
+ head, in this repository" with no relationship to the user. Before it, Kepler correlated a local branch by matching
243
+ the account-wide sweep, which holds only pull requests the user authored, is assigned or reviews — so a teammate's
244
+ pull request from the user's branch never correlated — and a cold card paid for that whole sweep before showing
245
+ anything. It matches on the head branch name in every state, so a merged pull request whose branch was deleted is
246
+ still found, and on the head repository, the base one or the `headOwner` fork, so a same-named branch in another
247
+ fork never correlates; a `headOwner` equal to the owner means the base repository, so Kepler can pass the owner of
248
+ the remote the branch was pushed to. Up to 10 per branch; an empty list without `truncated` is a proven none a
249
+ consumer can cache, and a failed target is dropped with `fetchFailed`. GitHub/GHE answer up to 25 branches per
250
+ request, and every other host costs one request per branch. GitLab and Azure DevOps resolve each match (typically
251
+ 0–1 per branch) through `getPullRequestsBatch`'s own read, so a pull request's `url`, and so its identity, is the
252
+ same whichever read found it. Bitbucket DC and Azure DevOps serve base-repository branches only and refuse a
253
+ `headOwner` naming another owner.
254
+
255
+ **Current account resolution.** `getCurrentAccount` answers who a git host connection is signed in as, on
256
+ demand. Before it, Kepler learned the viewer's account only when its `IntegrationManagerCacheProvider`
257
+ `getCurrentAccount` hook fired during some other read, so "is this pull request mine" could stay unanswered until
258
+ something else resolved the account. The read goes through that same hook, so there is still one cache. `account`
259
+ is never absent without a warning. Jira, Linear and Trello refuse: they have only a per-resource account.
213
260
 
214
261
  **Kepler-side follow-up:** `ProviderScopeFilter` carries a single `repo?: string` today and needs the criteria
215
262
  set; the `provider-data` adapter then routes "All visible" to `searchIssuesPage` + `countIssues`.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@gitkraken/core-gitlens",
3
3
  "description": "GitLens core — shared Git / AI / GitHub primitives for internal GitKraken consumption",
4
- "version": "0.7.2",
4
+ "version": "0.9.0",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "author": {
7
7
  "name": "GitKraken",
@@ -1,33 +0,0 @@
1
- import { IssueShape } from "../../../git/models/issue.js";
2
- import { IntegrationIds } from "../constants.js";
3
- import { ProviderResult } from "../results.js";
4
- import { ProviderReadContext } from "./context.js";
5
- //#region ../plus/integrations/src/reads/trackerIssue.d.ts
6
- export interface TrackerIssueResult {
7
- key: string;
8
- /**
9
- * The resolved issue, or `undefined` when it provably does not exist or is not visible to this connection.
10
- * A failed read returns no item and sets `fetchFailed`.
11
- */
12
- issue?: IssueShape;
13
- }
14
- /**
15
- * `domain` selects the host of a self-managed tracker (Jira Data Center), as it does for
16
- * `listIssueTrackerIssuesPage`, and is ignored for the cloud trackers, which have a single canonical host.
17
- *
18
- * Unlike its siblings, this read never falls back to the primary connection for a self-managed tracker: it
19
- * requires a `domain` or a `connectionId` with a configured host and refuses otherwise. Two self-hosted
20
- * instances routinely issue the same project and issue keys, and this read's `issue: undefined` is a proven
21
- * absence a caller may cache — an answer from whichever host happens to be primary would be cached under a key
22
- * that names a different instance (#5872).
23
- */
24
- export declare function getTrackerIssue(ctx: ProviderReadContext, options: {
25
- providerId: IntegrationIds;
26
- resourceId: string;
27
- resourceUrl?: string;
28
- key: string;
29
- connectionId?: string;
30
- domain?: string;
31
- }): Promise<ProviderResult<TrackerIssueResult>>;
32
- //#endregion
33
- //# sourceMappingURL=trackerIssue.d.ts.map
@@ -1,60 +0,0 @@
1
- import "../constants.js";
2
- import { areDomainsOnSameHost, hostFromDomain } from "../utils/domain.utils.js";
3
- import { isIssuesHostIntegrationId, isIssuesSelfManagedHostIntegrationId } from "../utils/integration.utils.js";
4
- import { issueTrackerOnlySurfaceWarning, otherWarning } from "./warnings.js";
5
- import { runCaptured } from "./drains.js";
6
- import { isIssuesIntegration } from "../models/issuesIntegration.js";
7
- //#region ../plus/integrations/src/reads/trackerIssue.ts
8
- /**
9
- * `domain` selects the host of a self-managed tracker (Jira Data Center), as it does for
10
- * `listIssueTrackerIssuesPage`, and is ignored for the cloud trackers, which have a single canonical host.
11
- *
12
- * Unlike its siblings, this read never falls back to the primary connection for a self-managed tracker: it
13
- * requires a `domain` or a `connectionId` with a configured host and refuses otherwise. Two self-hosted
14
- * instances routinely issue the same project and issue keys, and this read's `issue: undefined` is a proven
15
- * absence a caller may cache — an answer from whichever host happens to be primary would be cached under a key
16
- * that names a different instance (#5872).
17
- */
18
- async function getTrackerIssue(ctx, options) {
19
- const refused = (warning) => ({
20
- items: [],
21
- warnings: [warning],
22
- fetchFailed: true
23
- });
24
- const surface = "Issue resolution by key";
25
- if (!isIssuesHostIntegrationId(options.providerId)) return refused(issueTrackerOnlySurfaceWarning(options.providerId, options.connectionId, surface));
26
- if (options.resourceId.trim().length === 0) return refused(otherWarning(options.providerId, void 0, options.connectionId, `${surface} requires a resource id.`));
27
- if (options.key.trim().length === 0) return refused(otherWarning(options.providerId, void 0, options.connectionId, `${surface} requires an issue key.`));
28
- const resourceUrl = options.resourceUrl?.trim() || void 0;
29
- if (options.providerId === "jira" && resourceUrl == null) return refused(otherWarning(options.providerId, void 0, options.connectionId, `${surface} requires the Jira resource URL so the result contains a browser link without resource discovery.`));
30
- if (isIssuesSelfManagedHostIntegrationId(options.providerId) && (options.domain != null ? hostFromDomain(options.domain) == null : options.connectionId == null || !ctx.getConfigured(options.providerId).some((c) => c.id === options.connectionId && hostFromDomain(c.domain) != null))) return refused(otherWarning(options.providerId, void 0, options.connectionId, `${surface} requires a domain or a configured connection id for '${options.providerId}': every configured host can hold a different issue under the same key, so the read does not answer from the primary connection.`));
31
- const integration = await ctx.getIntegrationForRead(options.providerId, options.connectionId, options.domain);
32
- if (integration == null) {
33
- const early = ctx.earlyReturnConnectionWarnings(options.providerId, options.connectionId, options.domain);
34
- return {
35
- items: [],
36
- warnings: early.warnings,
37
- fetchFailed: early.fetchFailed || void 0
38
- };
39
- }
40
- if (!isIssuesIntegration(integration)) return refused(issueTrackerOnlySurfaceWarning(options.providerId, options.connectionId, surface));
41
- if (!integration.supportsIssueLookupByResourceId) return refused(otherWarning(options.providerId, void 0, options.connectionId, `${surface} is not supported by '${options.providerId}'; its single-issue read cannot prove an absence, so a miss would not be safe to cache.`));
42
- if (isIssuesSelfManagedHostIntegrationId(options.providerId) && !areDomainsOnSameHost(options.resourceId, integration.domain)) return refused(otherWarning(options.providerId, void 0, options.connectionId, `${surface} requires the resource id of '${options.providerId}' to name the host the read resolved to.`));
43
- const domain = ctx.domainForRead(integration, options.providerId, options.connectionId, options.domain);
44
- const issue = await runCaptured(options.providerId, domain, options.connectionId, () => integration.getIssueByResourceIdResult(options.resourceId, options.key, {
45
- connectionId: options.connectionId,
46
- resourceUrl
47
- }), { warnOnMissingSession: true });
48
- if (issue.warning != null) return refused(issue.warning);
49
- return {
50
- items: [{
51
- key: options.key,
52
- ...issue.value != null ? { issue: issue.value } : {}
53
- }],
54
- warnings: []
55
- };
56
- }
57
- //#endregion
58
- export { getTrackerIssue };
59
-
60
- //# sourceMappingURL=trackerIssue.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"trackerIssue.js","names":[],"sources":["../../../../../plus/integrations/src/reads/trackerIssue.ts"],"sourcesContent":["import type { IssueShape } from '@gitlens/git/models/issue.js';\nimport type { IntegrationIds } from '../constants.js';\nimport { IssuesCloudHostIntegrationId } from '../constants.js';\nimport { isIssuesIntegration } from '../models/issuesIntegration.js';\nimport type { ProviderResult, ProviderWarning } from '../results.js';\nimport { areDomainsOnSameHost, hostFromDomain } from '../utils/domain.utils.js';\nimport { isIssuesHostIntegrationId, isIssuesSelfManagedHostIntegrationId } from '../utils/integration.utils.js';\nimport type { ProviderReadContext } from './context.js';\nimport { runCaptured } from './drains.js';\nimport { issueTrackerOnlySurfaceWarning, otherWarning } from './warnings.js';\n\nexport interface TrackerIssueResult {\n\tkey: string;\n\t/**\n\t * The resolved issue, or `undefined` when it provably does not exist or is not visible to this connection.\n\t * A failed read returns no item and sets `fetchFailed`.\n\t */\n\tissue?: IssueShape;\n}\n\n/**\n * `domain` selects the host of a self-managed tracker (Jira Data Center), as it does for\n * `listIssueTrackerIssuesPage`, and is ignored for the cloud trackers, which have a single canonical host.\n *\n * Unlike its siblings, this read never falls back to the primary connection for a self-managed tracker: it\n * requires a `domain` or a `connectionId` with a configured host and refuses otherwise. Two self-hosted\n * instances routinely issue the same project and issue keys, and this read's `issue: undefined` is a proven\n * absence a caller may cache — an answer from whichever host happens to be primary would be cached under a key\n * that names a different instance (#5872).\n */\nexport async function getTrackerIssue(\n\tctx: ProviderReadContext,\n\toptions: {\n\t\tproviderId: IntegrationIds;\n\t\tresourceId: string;\n\t\tresourceUrl?: string;\n\t\tkey: string;\n\t\tconnectionId?: string;\n\t\tdomain?: string;\n\t},\n): Promise<ProviderResult<TrackerIssueResult>> {\n\tconst refused = (warning: ProviderWarning): ProviderResult<TrackerIssueResult> => ({\n\t\titems: [],\n\t\twarnings: [warning],\n\t\tfetchFailed: true,\n\t});\n\tconst surface = 'Issue resolution by key';\n\n\tif (!isIssuesHostIntegrationId(options.providerId)) {\n\t\treturn refused(issueTrackerOnlySurfaceWarning(options.providerId, options.connectionId, surface));\n\t}\n\n\tif (options.resourceId.trim().length === 0) {\n\t\treturn refused(\n\t\t\totherWarning(options.providerId, undefined, options.connectionId, `${surface} requires a resource id.`),\n\t\t);\n\t}\n\n\tif (options.key.trim().length === 0) {\n\t\treturn refused(\n\t\t\totherWarning(options.providerId, undefined, options.connectionId, `${surface} requires an issue key.`),\n\t\t);\n\t}\n\n\tconst resourceUrl = options.resourceUrl?.trim() || undefined;\n\tif (options.providerId === IssuesCloudHostIntegrationId.Jira && resourceUrl == null) {\n\t\treturn refused(\n\t\t\totherWarning(\n\t\t\t\toptions.providerId,\n\t\t\t\tundefined,\n\t\t\t\toptions.connectionId,\n\t\t\t\t`${surface} requires the Jira resource URL so the result contains a browser link without resource discovery.`,\n\t\t\t),\n\t\t);\n\t}\n\n\t// A `domain` only selects a host when it parses to one, and a `connectionId` only when it names a configured\n\t// connection that has one; anything else would resolve the primary host instead, which is the fallback this\n\t// read refuses.\n\tif (\n\t\tisIssuesSelfManagedHostIntegrationId(options.providerId) &&\n\t\t(options.domain != null\n\t\t\t? hostFromDomain(options.domain) == null\n\t\t\t: options.connectionId == null ||\n\t\t\t\t!ctx\n\t\t\t\t\t.getConfigured(options.providerId)\n\t\t\t\t\t.some(c => c.id === options.connectionId && hostFromDomain(c.domain) != null))\n\t) {\n\t\treturn refused(\n\t\t\totherWarning(\n\t\t\t\toptions.providerId,\n\t\t\t\tundefined,\n\t\t\t\toptions.connectionId,\n\t\t\t\t`${surface} requires a domain or a configured connection id for '${options.providerId}': every configured host can hold a different issue under the same key, so the read does not answer from the primary connection.`,\n\t\t\t),\n\t\t);\n\t}\n\n\tconst integration = await ctx.getIntegrationForRead(options.providerId, options.connectionId, options.domain);\n\tif (integration == null) {\n\t\t// A supplied connectionId or domain that no longer resolves is a broken target, not an empty account.\n\t\tconst early = ctx.earlyReturnConnectionWarnings(options.providerId, options.connectionId, options.domain);\n\t\treturn { items: [], warnings: early.warnings, fetchFailed: early.fetchFailed || undefined };\n\t}\n\tif (!isIssuesIntegration(integration)) {\n\t\treturn refused(issueTrackerOnlySurfaceWarning(options.providerId, options.connectionId, surface));\n\t}\n\tif (!integration.supportsIssueLookupByResourceId) {\n\t\treturn refused(\n\t\t\totherWarning(\n\t\t\t\toptions.providerId,\n\t\t\t\tundefined,\n\t\t\t\toptions.connectionId,\n\t\t\t\t`${surface} is not supported by '${options.providerId}'; its single-issue read cannot prove an absence, so a miss would not be safe to cache.`,\n\t\t\t),\n\t\t);\n\t}\n\n\t// A self-managed tracker's single resource is its host, and the read is keyed (and cached) by `resourceId`\n\t// while it is addressed to the host `domain`/`connectionId` resolved. When they disagree, host B's answer —\n\t// including a cacheable absence — would be stored under host A's key.\n\tif (\n\t\tisIssuesSelfManagedHostIntegrationId(options.providerId) &&\n\t\t!areDomainsOnSameHost(options.resourceId, integration.domain)\n\t) {\n\t\treturn refused(\n\t\t\totherWarning(\n\t\t\t\toptions.providerId,\n\t\t\t\tundefined,\n\t\t\t\toptions.connectionId,\n\t\t\t\t`${surface} requires the resource id of '${options.providerId}' to name the host the read resolved to.`,\n\t\t\t),\n\t\t);\n\t}\n\n\tconst domain = ctx.domainForRead(integration, options.providerId, options.connectionId, options.domain);\n\tconst issue = await runCaptured(\n\t\toptions.providerId,\n\t\tdomain,\n\t\toptions.connectionId,\n\t\t() =>\n\t\t\tintegration.getIssueByResourceIdResult(options.resourceId, options.key, {\n\t\t\t\tconnectionId: options.connectionId,\n\t\t\t\tresourceUrl: resourceUrl,\n\t\t\t}),\n\t\t{ warnOnMissingSession: true },\n\t);\n\tif (issue.warning != null) {\n\t\treturn refused(issue.warning);\n\t}\n\n\treturn {\n\t\titems: [{ key: options.key, ...(issue.value != null ? { issue: issue.value } : {}) }],\n\t\twarnings: [],\n\t};\n}\n"],"mappings":";;;;;;;;;;;;;;;;;AA8BA,eAAsB,gBACrB,KACA,SAQ8C;CAC9C,MAAM,WAAW,aAAkE;EAClF,OAAO,CAAC;EACR,UAAU,CAAC,OAAO;EAClB,aAAa;CACd;CACA,MAAM,UAAU;CAEhB,IAAI,CAAC,0BAA0B,QAAQ,UAAU,GAChD,OAAO,QAAQ,+BAA+B,QAAQ,YAAY,QAAQ,cAAc,OAAO,CAAC;CAGjG,IAAI,QAAQ,WAAW,KAAK,CAAC,CAAC,WAAW,GACxC,OAAO,QACN,aAAa,QAAQ,YAAY,KAAA,GAAW,QAAQ,cAAc,GAAG,QAAQ,yBAAyB,CACvG;CAGD,IAAI,QAAQ,IAAI,KAAK,CAAC,CAAC,WAAW,GACjC,OAAO,QACN,aAAa,QAAQ,YAAY,KAAA,GAAW,QAAQ,cAAc,GAAG,QAAQ,wBAAwB,CACtG;CAGD,MAAM,cAAc,QAAQ,aAAa,KAAK,KAAK,KAAA;CACnD,IAAI,QAAQ,eAAA,UAAoD,eAAe,MAC9E,OAAO,QACN,aACC,QAAQ,YACR,KAAA,GACA,QAAQ,cACR,GAAG,QAAQ,kGACZ,CACD;CAMD,IACC,qCAAqC,QAAQ,UAAU,MACtD,QAAQ,UAAU,OAChB,eAAe,QAAQ,MAAM,KAAK,OAClC,QAAQ,gBAAgB,QACzB,CAAC,IACC,cAAc,QAAQ,UAAU,CAAC,CACjC,MAAK,MAAK,EAAE,OAAO,QAAQ,gBAAgB,eAAe,EAAE,MAAM,KAAK,IAAI,IAE/E,OAAO,QACN,aACC,QAAQ,YACR,KAAA,GACA,QAAQ,cACR,GAAG,QAAQ,wDAAwD,QAAQ,WAAW,iIACvF,CACD;CAGD,MAAM,cAAc,MAAM,IAAI,sBAAsB,QAAQ,YAAY,QAAQ,cAAc,QAAQ,MAAM;CAC5G,IAAI,eAAe,MAAM;EAExB,MAAM,QAAQ,IAAI,8BAA8B,QAAQ,YAAY,QAAQ,cAAc,QAAQ,MAAM;EACxG,OAAO;GAAE,OAAO,CAAC;GAAG,UAAU,MAAM;GAAU,aAAa,MAAM,eAAe,KAAA;EAAU;CAC3F;CACA,IAAI,CAAC,oBAAoB,WAAW,GACnC,OAAO,QAAQ,+BAA+B,QAAQ,YAAY,QAAQ,cAAc,OAAO,CAAC;CAEjG,IAAI,CAAC,YAAY,iCAChB,OAAO,QACN,aACC,QAAQ,YACR,KAAA,GACA,QAAQ,cACR,GAAG,QAAQ,wBAAwB,QAAQ,WAAW,wFACvD,CACD;CAMD,IACC,qCAAqC,QAAQ,UAAU,KACvD,CAAC,qBAAqB,QAAQ,YAAY,YAAY,MAAM,GAE5D,OAAO,QACN,aACC,QAAQ,YACR,KAAA,GACA,QAAQ,cACR,GAAG,QAAQ,gCAAgC,QAAQ,WAAW,yCAC/D,CACD;CAGD,MAAM,SAAS,IAAI,cAAc,aAAa,QAAQ,YAAY,QAAQ,cAAc,QAAQ,MAAM;CACtG,MAAM,QAAQ,MAAM,YACnB,QAAQ,YACR,QACA,QAAQ,oBAEP,YAAY,2BAA2B,QAAQ,YAAY,QAAQ,KAAK;EACvE,cAAc,QAAQ;EACT;CACd,CAAC,GACF,EAAE,sBAAsB,KAAK,CAC9B;CACA,IAAI,MAAM,WAAW,MACpB,OAAO,QAAQ,MAAM,OAAO;CAG7B,OAAO;EACN,OAAO,CAAC;GAAE,KAAK,QAAQ;GAAK,GAAI,MAAM,SAAS,OAAO,EAAE,OAAO,MAAM,MAAM,IAAI,CAAC;EAAG,CAAC;EACpF,UAAU,CAAC;CACZ;AACD"}