@gitkraken/core-gitlens 0.3.0 → 0.3.1
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.
- package/CHANGELOG.md +8 -1
- package/dist/git/cache.d.ts +12 -6
- package/dist/git/cache.d.ts.map +1 -1
- package/dist/git/cache.js +40 -39
- package/dist/git/cache.js.map +1 -1
- package/dist/git/context.d.ts +19 -0
- package/dist/git/context.d.ts.map +1 -1
- package/dist/git/context.js.map +1 -1
- package/dist/git/features.d.ts +1 -1
- package/dist/git/features.d.ts.map +1 -1
- package/dist/git/features.js +1 -0
- package/dist/git/features.js.map +1 -1
- package/dist/git/models/graph.d.ts +96 -0
- package/dist/git/models/graph.d.ts.map +1 -1
- package/dist/git/models/graph.js +24 -1
- package/dist/git/models/graph.js.map +1 -1
- package/dist/git/models/remoteProvider.d.ts +1 -0
- package/dist/git/models/remoteProvider.d.ts.map +1 -1
- package/dist/git/models/remoteProvider.js.map +1 -1
- package/dist/git/models/repository.d.ts +21 -12
- package/dist/git/models/repository.d.ts.map +1 -1
- package/dist/git/models/repository.js +70 -84
- package/dist/git/models/repository.js.map +1 -1
- package/dist/git/models/statusFile.d.ts +9 -0
- package/dist/git/models/statusFile.d.ts.map +1 -1
- package/dist/git/models/statusFile.js +11 -0
- package/dist/git/models/statusFile.js.map +1 -1
- package/dist/git/providers/commits.d.ts +4 -0
- package/dist/git/providers/commits.d.ts.map +1 -1
- package/dist/git/providers/config.d.ts +1 -3
- package/dist/git/providers/config.d.ts.map +1 -1
- package/dist/git/providers/operations.d.ts +39 -13
- package/dist/git/providers/operations.d.ts.map +1 -1
- package/dist/git/remotes/custom.d.ts +2 -0
- package/dist/git/remotes/custom.d.ts.map +1 -1
- package/dist/git/remotes/custom.js +20 -0
- package/dist/git/remotes/custom.js.map +1 -1
- package/dist/git/utils/contributor.utils.d.ts +3 -0
- package/dist/git/utils/contributor.utils.d.ts.map +1 -1
- package/dist/git/utils/contributor.utils.js +22 -0
- package/dist/git/utils/contributor.utils.js.map +1 -1
- package/dist/git/utils/reachability.utils.d.ts +37 -0
- package/dist/git/utils/reachability.utils.d.ts.map +1 -0
- package/dist/git/utils/reachability.utils.js +96 -0
- package/dist/git/utils/reachability.utils.js.map +1 -0
- package/dist/git/watching/watchService.d.ts +4 -2
- package/dist/git/watching/watchService.d.ts.map +1 -1
- package/dist/git/watching/watchService.js +8 -3
- package/dist/git/watching/watchService.js.map +1 -1
- package/dist/git/watching/watchSession.d.ts +14 -12
- package/dist/git/watching/watchSession.d.ts.map +1 -1
- package/dist/git/watching/watchSession.js +56 -55
- package/dist/git/watching/watchSession.js.map +1 -1
- package/dist/git-cli/providers/branches.d.ts.map +1 -1
- package/dist/git-cli/providers/branches.js +1 -0
- package/dist/git-cli/providers/branches.js.map +1 -1
- package/dist/git-cli/providers/commits.d.ts +1 -0
- package/dist/git-cli/providers/commits.d.ts.map +1 -1
- package/dist/git-cli/providers/commits.js +12 -0
- package/dist/git-cli/providers/commits.js.map +1 -1
- package/dist/git-cli/providers/config.d.ts +8 -4
- package/dist/git-cli/providers/config.d.ts.map +1 -1
- package/dist/git-cli/providers/config.js +62 -39
- package/dist/git-cli/providers/config.js.map +1 -1
- package/dist/git-cli/providers/diff.d.ts.map +1 -1
- package/dist/git-cli/providers/diff.js +1 -1
- package/dist/git-cli/providers/diff.js.map +1 -1
- package/dist/git-cli/providers/graph.d.ts.map +1 -1
- package/dist/git-cli/providers/graph.js +132 -14
- package/dist/git-cli/providers/graph.js.map +1 -1
- package/dist/git-cli/providers/operations.d.ts +6 -5
- package/dist/git-cli/providers/operations.d.ts.map +1 -1
- package/dist/git-cli/providers/operations.js +97 -95
- package/dist/git-cli/providers/operations.js.map +1 -1
- package/dist/git-cli/providers/stash.d.ts.map +1 -1
- package/dist/git-cli/providers/stash.js +20 -2
- package/dist/git-cli/providers/stash.js.map +1 -1
- package/dist/git-cli/providers/status.d.ts.map +1 -1
- package/dist/git-cli/providers/status.js +5 -2
- package/dist/git-cli/providers/status.js.map +1 -1
- package/dist/ipc/discovery.d.ts +22 -0
- package/dist/ipc/discovery.d.ts.map +1 -1
- package/dist/ipc/discovery.js +111 -2
- package/dist/ipc/discovery.js.map +1 -1
- package/dist/plus/agents/providers/claudeCodeProvider.d.ts +73 -20
- package/dist/plus/agents/providers/claudeCodeProvider.d.ts.map +1 -1
- package/dist/plus/agents/providers/claudeCodeProvider.js +394 -88
- package/dist/plus/agents/providers/claudeCodeProvider.js.map +1 -1
- package/dist/plus/agents/types.d.ts +62 -11
- package/dist/plus/agents/types.d.ts.map +1 -1
- package/dist/plus/ai/models/model.d.ts +1 -1
- package/dist/plus/ai/models/model.d.ts.map +1 -1
- package/dist/plus/ai/models/model.js.map +1 -1
- package/dist/plus/ai/models/provider.d.ts +3 -0
- package/dist/plus/ai/models/provider.d.ts.map +1 -1
- package/dist/plus/ai/providers/gitkrakenProvider.d.ts +1 -1
- package/dist/plus/ai/providers/gitkrakenProvider.d.ts.map +1 -1
- package/dist/plus/ai/providers/gitkrakenProvider.js +4 -1
- package/dist/plus/ai/providers/gitkrakenProvider.js.map +1 -1
- package/dist/plus/ai/providers/openAICompatibleProviderBase.d.ts +4 -3
- package/dist/plus/ai/providers/openAICompatibleProviderBase.d.ts.map +1 -1
- package/dist/plus/ai/providers/openAICompatibleProviderBase.js +9 -6
- package/dist/plus/ai/providers/openAICompatibleProviderBase.js.map +1 -1
- package/dist/plus/ai/utils/ai.utils.d.ts.map +1 -1
- package/dist/plus/ai/utils/ai.utils.js +2 -0
- package/dist/plus/ai/utils/ai.utils.js.map +1 -1
- package/dist/plus/git-github/api/github.d.ts +2 -0
- package/dist/plus/git-github/api/github.d.ts.map +1 -1
- package/dist/plus/git-github/api/github.js +93 -6
- package/dist/plus/git-github/api/github.js.map +1 -1
- package/dist/plus/git-github/providers/github/refs.js +1 -1
- package/dist/utils/date.d.ts.map +1 -1
- package/dist/utils/date.js +6 -0
- package/dist/utils/date.js.map +1 -1
- package/dist/utils/debounce.js +1 -1
- package/dist/utils/iterable.d.ts +7 -1
- package/dist/utils/iterable.d.ts.map +1 -1
- package/dist/utils/iterable.js +12 -4
- package/dist/utils/iterable.js.map +1 -1
- package/dist/utils/object.d.ts +22 -1
- package/dist/utils/object.d.ts.map +1 -1
- package/dist/utils/object.js +56 -3
- package/dist/utils/object.js.map +1 -1
- package/dist/utils/paging.js +14 -15
- package/dist/utils/paging.js.map +1 -1
- package/dist/utils/promiseCache.d.ts.map +1 -1
- package/dist/utils/promiseCache.js +12 -4
- package/dist/utils/promiseCache.js.map +1 -1
- package/dist/utils/string.js +1 -1
- package/package.json +1 -1
- package/src/git/cache.ts +58 -70
- package/src/git/context.ts +21 -0
- package/src/git/features.ts +2 -0
- package/src/git/models/graph.ts +97 -0
- package/src/git/models/remoteProvider.ts +1 -0
- package/src/git/models/repository.ts +84 -93
- package/src/git/models/statusFile.ts +12 -0
- package/src/git/providers/commits.ts +4 -0
- package/src/git/providers/config.ts +1 -5
- package/src/git/providers/operations.ts +42 -22
- package/src/git/remotes/custom.ts +23 -0
- package/src/git/utils/contributor.utils.ts +24 -0
- package/src/git/utils/reachability.utils.ts +109 -0
- package/src/git/watching/watchService.ts +9 -4
- package/src/git/watching/watchSession.ts +61 -55
- package/src/git-cli/providers/branches.ts +1 -0
- package/src/git-cli/providers/commits.ts +24 -0
- package/src/git-cli/providers/config.ts +63 -49
- package/src/git-cli/providers/diff.ts +2 -1
- package/src/git-cli/providers/graph.ts +145 -14
- package/src/git-cli/providers/operations.ts +118 -130
- package/src/git-cli/providers/stash.ts +20 -2
- package/src/git-cli/providers/status.ts +11 -3
- package/src/ipc/discovery.ts +132 -2
- package/src/plus/agents/providers/claudeCodeProvider.ts +430 -90
- package/src/plus/agents/types.ts +59 -11
- package/src/plus/ai/models/model.ts +1 -0
- package/src/plus/ai/models/provider.ts +7 -1
- package/src/plus/ai/providers/gitkrakenProvider.ts +4 -0
- package/src/plus/ai/providers/openAICompatibleProviderBase.ts +21 -4
- package/src/plus/ai/utils/ai.utils.ts +2 -0
- package/src/plus/git-github/api/github.ts +120 -8
- package/src/plus/git-github/providers/github/refs.ts +1 -1
- package/src/utils/date.ts +5 -0
- package/src/utils/debounce.ts +1 -1
- package/src/utils/iterable.ts +14 -3
- package/src/utils/object.ts +55 -4
- package/src/utils/paging.ts +14 -14
- package/src/utils/promiseCache.ts +24 -16
- package/src/utils/string.ts +1 -1
|
@@ -32,9 +32,9 @@ import {
|
|
|
32
32
|
getRemoteNameFromBranchName,
|
|
33
33
|
} from '../../git/utils/branch.utils.js';
|
|
34
34
|
import { getChangedFilesCount } from '../../git/utils/commit.utils.js';
|
|
35
|
+
import { createReachabilityTableBuilder } from '../../git/utils/reachability.utils.js';
|
|
35
36
|
import { isUncommitted } from '../../git/utils/revision.utils.js';
|
|
36
37
|
import { getSearchQueryComparisonKey, parseSearchQueryGitCommand } from '../../git/utils/search.utils.js';
|
|
37
|
-
import { compareReachableRefs } from '../../git/utils/sorting.js';
|
|
38
38
|
import { getTagId } from '../../git/utils/tag.utils.js';
|
|
39
39
|
import { isUserMatch } from '../../git/utils/user.utils.js';
|
|
40
40
|
import { getWorktreeId, groupWorktreesByBranch } from '../../git/utils/worktree.utils.js';
|
|
@@ -119,9 +119,14 @@ export class GraphGitSubProvider implements GitGraphSubProvider {
|
|
|
119
119
|
const [worktrees, worktreesByBranch] = getSettledValue(worktreesResult) ?? [[], new Map<string, GitWorktree>()];
|
|
120
120
|
|
|
121
121
|
let branchIdOfMainWorktree: string | undefined;
|
|
122
|
+
let mainWorktree: GitWorktree | undefined;
|
|
122
123
|
if (worktreesByBranch != null) {
|
|
123
|
-
|
|
124
|
-
if (
|
|
124
|
+
const defaultEntry = find(worktreesByBranch, ([, wt]) => wt.isDefault);
|
|
125
|
+
if (defaultEntry != null) {
|
|
126
|
+
[branchIdOfMainWorktree, mainWorktree] = defaultEntry;
|
|
127
|
+
// Remove the main/default worktree so the branch-row decoration shows `+checkedout`
|
|
128
|
+
// (not `+worktree`) for the main checkout. `mainWorktree` is retained below purely so
|
|
129
|
+
// Undo Commit can still reach the main worktree's HEAD from a secondary worktree's graph.
|
|
125
130
|
worktreesByBranch.delete(branchIdOfMainWorktree);
|
|
126
131
|
}
|
|
127
132
|
}
|
|
@@ -142,9 +147,62 @@ export class GraphGitSubProvider implements GitGraphSubProvider {
|
|
|
142
147
|
const avatars = new Map<string, string>();
|
|
143
148
|
const ids = new Set<string>();
|
|
144
149
|
const reachableFromHEAD = new Set<string>();
|
|
150
|
+
// SHAs on the first-parent chain from HEAD up to (excluding) the first merge commit — the only
|
|
151
|
+
// commits a plain (non-`--rebase-merges`) interactive rebase can safely rewrite. `headSha` seeds
|
|
152
|
+
// the chain; `rewriteableNextSha` tracks the next sha expected on it. Outer scope so they persist
|
|
153
|
+
// across `more()` pagination, like `reachableFromHEAD`.
|
|
154
|
+
const rewriteableFromHEAD = new Set<string>();
|
|
155
|
+
let headSha: string | undefined;
|
|
156
|
+
let rewriteableNextSha: string | undefined;
|
|
157
|
+
// Undo Commit is offered only on a worktree HEAD that is a LEAF (nothing is built on it) —
|
|
158
|
+
// undoing a commit other work is stacked on is unsafe. The leaf check is only needed for the
|
|
159
|
+
// undo-eligible tips (the active HEAD + each worktree's HEAD), so track just those shas rather
|
|
160
|
+
// than every commit, keeping this O(#worktrees) instead of O(#commits).
|
|
161
|
+
const undoableTipShas = new Set<string>();
|
|
162
|
+
if (headBranch?.sha != null) {
|
|
163
|
+
undoableTipShas.add(headBranch.sha);
|
|
164
|
+
}
|
|
165
|
+
for (const wt of worktrees) {
|
|
166
|
+
if (wt.sha != null) {
|
|
167
|
+
undoableTipShas.add(wt.sha);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
// The subset of `undoableTipShas` that turn out to have a child (are an ancestor of another
|
|
171
|
+
// commit) — i.e. NOT leaves. Built newest-first during the walk, so by the time a tip's row is
|
|
172
|
+
// processed every newer commit (its only possible children) has been seen. Undo is withheld for
|
|
173
|
+
// shas recorded here. Stash rows are excluded (a stash sitting on a tip must not block undoing it).
|
|
174
|
+
const tipShasWithChildren = new Set<string>();
|
|
175
|
+
// SHAs reachable from HEAD's tracking upstream tip. Combined with `reachableFromHEAD`,
|
|
176
|
+
// this lets us mark commits as unpushed (reachable from HEAD but not from HEAD's upstream).
|
|
177
|
+
const reachableFromHeadUpstream = new Set<string>();
|
|
145
178
|
|
|
146
179
|
// Map<sha, Map<refKey, ref>> — inner map deduplicates refs during propagation
|
|
147
180
|
const reachableRefs = new Map<string, Map<string, ReachableRef>>();
|
|
181
|
+
|
|
182
|
+
// Stable, append-only reachability table built as we walk — the PRIMARY representation shipped to
|
|
183
|
+
// consumers. Lives in the outer scope so it accumulates across `more()` pagination; indices
|
|
184
|
+
// already assigned never change, since git reachability only propagates to older (later-walked)
|
|
185
|
+
// commits. Per-row ref arrays are NOT retained — each row keeps only its set index
|
|
186
|
+
// (`contexts.reachabilityIndex`), and the working `reachableRefs` entry is dropped once emitted.
|
|
187
|
+
// The encoder lives next to its decoder in `reachability.utils` so the wire format can't drift.
|
|
188
|
+
const reachabilityBuilder = createReachabilityTableBuilder();
|
|
189
|
+
|
|
190
|
+
function finalizeRowReachability(
|
|
191
|
+
row: GitGraphRow,
|
|
192
|
+
sha: string,
|
|
193
|
+
refs: Map<string, ReachableRef> | undefined,
|
|
194
|
+
): void {
|
|
195
|
+
const setIndex = reachabilityBuilder.intern(refs?.values());
|
|
196
|
+
if (setIndex != null) {
|
|
197
|
+
(row.contexts ??= {}).reachabilityIndex = setIndex;
|
|
198
|
+
}
|
|
199
|
+
// Drop the transient per-row arrays so emitted rows don't retain reachability, and release the
|
|
200
|
+
// working entry — once a commit is emitted its children are all walked and its parents already
|
|
201
|
+
// seeded, so nothing reads `reachableRefs.get(sha)` again (orderings keep parents after children).
|
|
202
|
+
row.reachability = undefined;
|
|
203
|
+
reachableRefs.delete(sha);
|
|
204
|
+
}
|
|
205
|
+
|
|
148
206
|
const rowStats: GitGraphRowsStats = new Map<string, GitGraphRowStats>();
|
|
149
207
|
let pendingRowsStatsCount = 0;
|
|
150
208
|
let iterations = 0;
|
|
@@ -224,6 +282,12 @@ export class GraphGitSubProvider implements GitGraphSubProvider {
|
|
|
224
282
|
branchIdOfMainWorktree: branchIdOfMainWorktree,
|
|
225
283
|
stashes: gitStash?.stashes,
|
|
226
284
|
reachableFromHEAD: reachableFromHEAD,
|
|
285
|
+
rewriteableFromHEAD: rewriteableFromHEAD,
|
|
286
|
+
tipShasWithChildren: tipShasWithChildren,
|
|
287
|
+
// `undefined` when HEAD has no upstream, so the processor flags nothing as
|
|
288
|
+
// unpublished; otherwise the live set (mutated during the walk, read by ref).
|
|
289
|
+
reachableFromHeadUpstream:
|
|
290
|
+
headRefUpstreamName != null ? reachableFromHeadUpstream : undefined,
|
|
227
291
|
avatars: avatars,
|
|
228
292
|
}
|
|
229
293
|
: undefined;
|
|
@@ -288,6 +352,7 @@ export class GraphGitSubProvider implements GitGraphSubProvider {
|
|
|
288
352
|
if (tip.startsWith('HEAD')) {
|
|
289
353
|
head = true;
|
|
290
354
|
reachableFromHEAD.add(shaOrRemapped);
|
|
355
|
+
headSha ??= shaOrRemapped;
|
|
291
356
|
|
|
292
357
|
if (tip !== 'HEAD') {
|
|
293
358
|
tip = tip.substring(8);
|
|
@@ -302,12 +367,16 @@ export class GraphGitSubProvider implements GitGraphSubProvider {
|
|
|
302
367
|
if (branchName === 'HEAD') continue;
|
|
303
368
|
|
|
304
369
|
remoteBranchId = getBranchId(repoPath, true, tip);
|
|
370
|
+
const isHeadUpstream = tip === headRefUpstreamName;
|
|
371
|
+
if (isHeadUpstream) {
|
|
372
|
+
reachableFromHeadUpstream.add(shaOrRemapped);
|
|
373
|
+
}
|
|
305
374
|
refRemoteHead = {
|
|
306
375
|
id: remoteBranchId,
|
|
307
376
|
name: branchName,
|
|
308
377
|
owner: remote.name,
|
|
309
378
|
url: remote.url,
|
|
310
|
-
current:
|
|
379
|
+
current: isHeadUpstream,
|
|
311
380
|
hostingServiceType: remote.provider?.gkProviderId,
|
|
312
381
|
};
|
|
313
382
|
refRemoteHeads.push(refRemoteHead);
|
|
@@ -318,7 +387,16 @@ export class GraphGitSubProvider implements GitGraphSubProvider {
|
|
|
318
387
|
|
|
319
388
|
branch = branchMap.get(tip);
|
|
320
389
|
branchId = branch?.id ?? getBranchId(repoPath, false, tip);
|
|
321
|
-
|
|
390
|
+
// `worktreesByBranch` has the main/default worktree removed (above), so recover it
|
|
391
|
+
// here — Undo Commit needs to reach the main worktree's HEAD even when the active
|
|
392
|
+
// workspace is a secondary worktree.
|
|
393
|
+
const worktree =
|
|
394
|
+
worktreesByBranch?.get(branchId) ??
|
|
395
|
+
(branchId === branchIdOfMainWorktree ? mainWorktree : undefined);
|
|
396
|
+
const worktreeRef =
|
|
397
|
+
worktree != null
|
|
398
|
+
? { id: getWorktreeId(repoPath, worktree.name), path: worktree.path }
|
|
399
|
+
: undefined;
|
|
322
400
|
refHead = {
|
|
323
401
|
id: branchId,
|
|
324
402
|
name: tip,
|
|
@@ -330,7 +408,12 @@ export class GraphGitSubProvider implements GitGraphSubProvider {
|
|
|
330
408
|
id: getBranchId(repoPath, true, branch.upstream.name),
|
|
331
409
|
}
|
|
332
410
|
: undefined,
|
|
333
|
-
|
|
411
|
+
worktree: worktreeRef,
|
|
412
|
+
// Mirror `worktree.id` for `@gitkraken/gitkraken-components` — its bundled renderer
|
|
413
|
+
// reads `head.worktreeId` to pick the WORKTREE vs HEAD ref-badge. Scope to NON-default
|
|
414
|
+
// worktrees so the main checkout keeps its HEAD badge even though `worktree` (above)
|
|
415
|
+
// is populated for it for Undo routing.
|
|
416
|
+
worktreeId: worktree?.isDefault === false ? worktreeRef?.id : undefined,
|
|
334
417
|
};
|
|
335
418
|
refHeads.push(refHead);
|
|
336
419
|
if (branch?.upstream?.name != null) {
|
|
@@ -353,6 +436,27 @@ export class GraphGitSubProvider implements GitGraphSubProvider {
|
|
|
353
436
|
}
|
|
354
437
|
}
|
|
355
438
|
|
|
439
|
+
// First-parent rewriteable chain from HEAD: include each commit only while HEAD and
|
|
440
|
+
// every commit down to it has exactly one parent. Stops at (excludes) the first merge
|
|
441
|
+
// and the root — mirrors GitKraken's getDistinctCommitsFromHeadToFirstMergeCommit.
|
|
442
|
+
// Off-chain commits (a merge's other-parent ancestry, or anything below the first merge)
|
|
443
|
+
// are reachable-from-HEAD but NOT safely history-rewriteable by a plain interactive
|
|
444
|
+
// rebase. Relies on the same "commit emitted before its parents" ordering as above.
|
|
445
|
+
if (shaOrRemapped === headSha || shaOrRemapped === rewriteableNextSha) {
|
|
446
|
+
if (parents.length === 1) {
|
|
447
|
+
rewriteableFromHEAD.add(shaOrRemapped);
|
|
448
|
+
rewriteableNextSha = parents[0];
|
|
449
|
+
} else {
|
|
450
|
+
// A merge (>1 parents) or the root (0 parents) terminates the chain.
|
|
451
|
+
rewriteableNextSha = undefined;
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
if (reachableFromHeadUpstream.has(shaOrRemapped)) {
|
|
455
|
+
for (parent of parents) {
|
|
456
|
+
reachableFromHeadUpstream.add(parent);
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
356
460
|
// Seed reachability from all ref types on this commit
|
|
357
461
|
if (refHeads.length > 0 || refRemoteHeads.length > 0 || refTags.length > 0) {
|
|
358
462
|
let refs = reachableRefs.get(shaOrRemapped);
|
|
@@ -406,12 +510,13 @@ export class GraphGitSubProvider implements GitGraphSubProvider {
|
|
|
406
510
|
heads: refHeads,
|
|
407
511
|
remotes: refRemoteHeads,
|
|
408
512
|
tags: refTags,
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
513
|
+
// Transient: the row processor's `+unique` decision reads this; stripped just below.
|
|
514
|
+
// Unsorted (the consumer re-sorts after decoding) — order doesn't affect interning.
|
|
515
|
+
reachability: refs?.size ? { partial: true, refs: [...refs.values()] } : undefined,
|
|
412
516
|
isCurrentUser: true,
|
|
413
517
|
};
|
|
414
518
|
rowProcessor?.processRow(row, graphCtx!);
|
|
519
|
+
finalizeRowReachability(row, shaOrRemapped, refs);
|
|
415
520
|
rows.push(row);
|
|
416
521
|
|
|
417
522
|
if (stash.stats != null) {
|
|
@@ -424,6 +529,17 @@ export class GraphGitSubProvider implements GitGraphSubProvider {
|
|
|
424
529
|
} else {
|
|
425
530
|
isCurrentUser = isUserMatch(currentUser, commit.author, commit.authorEmail);
|
|
426
531
|
|
|
532
|
+
// Mark any undo-eligible tip that THIS (non-stash) commit builds on as non-leaf, so
|
|
533
|
+
// its Undo Commit is withheld. Uses the full parent set (incl. merge second-parents)
|
|
534
|
+
// but only records shas that are actually undo tips — newer commits are walked first,
|
|
535
|
+
// so a tip is recorded before its own row is processed. Stash rows are intentionally
|
|
536
|
+
// not handled here, so a stash based on a tip doesn't block undoing that tip.
|
|
537
|
+
for (parent of parents) {
|
|
538
|
+
if (undoableTipShas.has(parent)) {
|
|
539
|
+
tipShasWithChildren.add(parent);
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
|
|
427
543
|
const refs = reachableRefs.get(shaOrRemapped);
|
|
428
544
|
const row: GitGraphRow = {
|
|
429
545
|
sha: shaOrRemapped,
|
|
@@ -437,12 +553,13 @@ export class GraphGitSubProvider implements GitGraphSubProvider {
|
|
|
437
553
|
heads: refHeads,
|
|
438
554
|
remotes: refRemoteHeads,
|
|
439
555
|
tags: refTags,
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
556
|
+
// Transient: the row processor's `+unique` decision reads this; stripped just below.
|
|
557
|
+
// Unsorted (the consumer re-sorts after decoding) — order doesn't affect interning.
|
|
558
|
+
reachability: refs?.size ? { partial: true, refs: [...refs.values()] } : undefined,
|
|
443
559
|
isCurrentUser: isCurrentUser || undefined,
|
|
444
560
|
};
|
|
445
561
|
rowProcessor?.processRow(row, graphCtx!);
|
|
562
|
+
finalizeRowReachability(row, shaOrRemapped, refs);
|
|
446
563
|
rows.push(row);
|
|
447
564
|
|
|
448
565
|
if (commit.stats != null) {
|
|
@@ -460,7 +577,7 @@ export class GraphGitSubProvider implements GitGraphSubProvider {
|
|
|
460
577
|
if (deferStats) {
|
|
461
578
|
pendingRowsStatsCount++;
|
|
462
579
|
|
|
463
|
-
//
|
|
580
|
+
// oxlint-disable-next-line no-async-promise-executor
|
|
464
581
|
const promise = new Promise<void>(async resolve => {
|
|
465
582
|
try {
|
|
466
583
|
const args = [...statsParser.arguments];
|
|
@@ -514,6 +631,8 @@ export class GraphGitSubProvider implements GitGraphSubProvider {
|
|
|
514
631
|
worktrees: worktrees,
|
|
515
632
|
worktreesByBranch: worktreesByBranch,
|
|
516
633
|
reachableFromHEAD: reachableFromHEAD,
|
|
634
|
+
rewriteableFromHEAD: rewriteableFromHEAD,
|
|
635
|
+
reachability: reachabilityBuilder.build(),
|
|
517
636
|
rows: rows,
|
|
518
637
|
id: sha ?? rev,
|
|
519
638
|
rowsStats: rowStats,
|
|
@@ -538,7 +657,19 @@ export class GraphGitSubProvider implements GitGraphSubProvider {
|
|
|
538
657
|
}
|
|
539
658
|
}
|
|
540
659
|
|
|
541
|
-
|
|
660
|
+
let graph = await getCommitsForGraphCore.call(this, defaultLimit, selectSha, undefined, cancellation);
|
|
661
|
+
|
|
662
|
+
// Spurious-empty guard: the repo has branches (so `git log --all` should yield commits) yet the
|
|
663
|
+
// log returned no rows. That's an inconsistent result — almost always a transient ref-read race
|
|
664
|
+
// while a concurrent git operation rewrites refs/packed-refs — not a genuinely empty repo.
|
|
665
|
+
// Returning it would flash "No commits" in the graph. Retry once (transient glitches clear within
|
|
666
|
+
// ms). A truly empty repo has no branches and is left untouched.
|
|
667
|
+
if (graph.rows.length === 0 && branches?.length && !cancellation?.aborted) {
|
|
668
|
+
scope?.warn(`graph returned 0 rows but repo has ${branches.length} branches; retrying`);
|
|
669
|
+
graph = await getCommitsForGraphCore.call(this, defaultLimit, selectSha, undefined, cancellation);
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
return graph;
|
|
542
673
|
}
|
|
543
674
|
|
|
544
675
|
@debug({
|
|
@@ -25,6 +25,7 @@ import { getBranchNameAndRemote, getBranchTrackingWithoutRemote } from '../../gi
|
|
|
25
25
|
import { isBranchReference } from '../../git/utils/reference.utils.js';
|
|
26
26
|
import { debug } from '../../utils/decorators/log.js';
|
|
27
27
|
import { sequentialize } from '../../utils/decorators/sequentialize.js';
|
|
28
|
+
import { chunkByStringLength } from '../../utils/iterable.js';
|
|
28
29
|
import { Logger } from '../../utils/logger.js';
|
|
29
30
|
import { getScopedLogger } from '../../utils/logger.scoped.js';
|
|
30
31
|
import { normalizePath, splitPath } from '../../utils/path.js';
|
|
@@ -51,124 +52,22 @@ export class OperationsGitSubProvider implements GitOperationsSubProvider {
|
|
|
51
52
|
async checkout(
|
|
52
53
|
repoPath: string,
|
|
53
54
|
ref: string,
|
|
54
|
-
options?: { createBranch?: string }
|
|
55
|
+
options?: { createBranch?: string },
|
|
55
56
|
runOptions?: GitOperationRunOptions,
|
|
56
57
|
): Promise<void> {
|
|
57
58
|
const scope = getScopedLogger();
|
|
58
59
|
|
|
59
|
-
try {
|
|
60
|
-
await this.checkoutCore(repoPath, ref, options, runOptions);
|
|
61
|
-
this.context.hooks?.cache?.onReset?.(repoPath, 'branches', 'status');
|
|
62
|
-
this.context.hooks?.repository?.onChanged?.(repoPath, ['head', 'heads', 'index']);
|
|
63
|
-
} catch (ex) {
|
|
64
|
-
scope?.error(ex);
|
|
65
|
-
throw ex;
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
private async checkoutCore(
|
|
70
|
-
repoPath: string,
|
|
71
|
-
ref: string,
|
|
72
|
-
{ createBranch, path }: { createBranch?: string; path?: string } = {},
|
|
73
|
-
runOptions?: GitOperationRunOptions,
|
|
74
|
-
): Promise<void> {
|
|
75
60
|
const params = ['checkout'];
|
|
76
|
-
if (createBranch) {
|
|
77
|
-
params.push('-b', createBranch, ref, '--');
|
|
61
|
+
if (options?.createBranch) {
|
|
62
|
+
params.push('-b', options.createBranch, ref, '--');
|
|
78
63
|
} else {
|
|
79
64
|
params.push(ref, '--');
|
|
80
|
-
|
|
81
|
-
if (path) {
|
|
82
|
-
[path, repoPath] = splitPath(path, repoPath, true);
|
|
83
|
-
params.push(path);
|
|
84
|
-
}
|
|
85
65
|
}
|
|
86
66
|
|
|
87
67
|
try {
|
|
88
|
-
await this.git.run({ cwd: repoPath, ...runOptions }, ...params);
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
'checkout',
|
|
92
|
-
ex,
|
|
93
|
-
reason =>
|
|
94
|
-
new CheckoutError(
|
|
95
|
-
{ reason: reason ?? 'other', ref: ref, gitCommand: { repoPath: repoPath, args: params } },
|
|
96
|
-
ex,
|
|
97
|
-
),
|
|
98
|
-
);
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
@debug()
|
|
103
|
-
async checkoutConflictedPath(
|
|
104
|
-
repoPath: string,
|
|
105
|
-
path: string,
|
|
106
|
-
side: 'ours' | 'theirs',
|
|
107
|
-
runOptions?: GitOperationRunOptions,
|
|
108
|
-
): Promise<void> {
|
|
109
|
-
const scope = getScopedLogger();
|
|
110
|
-
|
|
111
|
-
[path, repoPath] = splitPath(path, repoPath, true);
|
|
112
|
-
const params = ['checkout', `--${side}`, '--', path];
|
|
113
|
-
|
|
114
|
-
try {
|
|
115
|
-
await this.git.run({ cwd: repoPath, ...runOptions }, ...params);
|
|
116
|
-
this.context.hooks?.cache?.onReset?.(repoPath, 'status');
|
|
117
|
-
this.context.hooks?.repository?.onChanged?.(repoPath, ['index']);
|
|
118
|
-
} catch (ex) {
|
|
119
|
-
scope?.error(ex);
|
|
120
|
-
throw getGitCommandError(
|
|
121
|
-
'checkout',
|
|
122
|
-
ex,
|
|
123
|
-
reason =>
|
|
124
|
-
new CheckoutError(
|
|
125
|
-
{ reason: reason ?? 'other', ref: side, gitCommand: { repoPath: repoPath, args: params } },
|
|
126
|
-
ex,
|
|
127
|
-
),
|
|
128
|
-
);
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
@debug()
|
|
133
|
-
async checkoutConflictedPaths(
|
|
134
|
-
repoPath: string,
|
|
135
|
-
paths: string[],
|
|
136
|
-
side: 'ours' | 'theirs',
|
|
137
|
-
runOptions?: GitOperationRunOptions,
|
|
138
|
-
): Promise<void> {
|
|
139
|
-
if (!paths.length) return;
|
|
140
|
-
|
|
141
|
-
const scope = getScopedLogger();
|
|
142
|
-
const normalized = paths.map(p => splitPath(p, repoPath, true)[0]);
|
|
143
|
-
|
|
144
|
-
// Build batches by accumulating the length of each path + a small per-arg overhead (separator +
|
|
145
|
-
// possible quoting), so a single long path — or just the overhead itself — can't push a batch
|
|
146
|
-
// past the CLI length limit. Always keep at least one path per batch, even if a lone path is
|
|
147
|
-
// longer than the limit (the OS may still accept it on some platforms).
|
|
148
|
-
const perArgOverhead = 3;
|
|
149
|
-
const batches: string[][] = [];
|
|
150
|
-
let current: string[] = [];
|
|
151
|
-
let currentLen = 0;
|
|
152
|
-
for (const p of normalized) {
|
|
153
|
-
const cost = p.length + perArgOverhead;
|
|
154
|
-
if (current.length > 0 && currentLen + cost > maxGitCliLength) {
|
|
155
|
-
batches.push(current);
|
|
156
|
-
current = [];
|
|
157
|
-
currentLen = 0;
|
|
158
|
-
}
|
|
159
|
-
current.push(p);
|
|
160
|
-
currentLen += cost;
|
|
161
|
-
}
|
|
162
|
-
if (current.length) {
|
|
163
|
-
batches.push(current);
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
try {
|
|
167
|
-
for (const batch of batches) {
|
|
168
|
-
await this.git.run({ cwd: repoPath, ...runOptions }, 'checkout', `--${side}`, '--', ...batch);
|
|
169
|
-
}
|
|
170
|
-
this.context.hooks?.cache?.onReset?.(repoPath, 'status');
|
|
171
|
-
this.context.hooks?.repository?.onChanged?.(repoPath, ['index']);
|
|
68
|
+
await this.git.run({ cwd: repoPath, errors: 'throw', ...runOptions }, ...params);
|
|
69
|
+
this.context.hooks?.cache?.onReset?.(repoPath, 'branches', 'status');
|
|
70
|
+
this.context.hooks?.repository?.onChanged?.(repoPath, ['head', 'heads', 'index']);
|
|
172
71
|
} catch (ex) {
|
|
173
72
|
scope?.error(ex);
|
|
174
73
|
throw getGitCommandError(
|
|
@@ -176,11 +75,7 @@ export class OperationsGitSubProvider implements GitOperationsSubProvider {
|
|
|
176
75
|
ex,
|
|
177
76
|
reason =>
|
|
178
77
|
new CheckoutError(
|
|
179
|
-
{
|
|
180
|
-
reason: reason ?? 'other',
|
|
181
|
-
ref: side,
|
|
182
|
-
gitCommand: { repoPath: repoPath, args: ['checkout', `--${side}`, '--', ...normalized] },
|
|
183
|
-
},
|
|
78
|
+
{ reason: reason ?? 'other', ref: ref, gitCommand: { repoPath: repoPath, args: params } },
|
|
184
79
|
ex,
|
|
185
80
|
),
|
|
186
81
|
);
|
|
@@ -383,7 +278,7 @@ export class OperationsGitSubProvider implements GitOperationsSubProvider {
|
|
|
383
278
|
}
|
|
384
279
|
|
|
385
280
|
try {
|
|
386
|
-
await this.git.run({ cwd: repoPath, ...runOptions }, ...params);
|
|
281
|
+
await this.git.run({ cwd: repoPath, errors: 'throw', ...runOptions }, ...params);
|
|
387
282
|
} catch (ex) {
|
|
388
283
|
throw getGitCommandError(
|
|
389
284
|
'fetch',
|
|
@@ -530,7 +425,7 @@ export class OperationsGitSubProvider implements GitOperationsSubProvider {
|
|
|
530
425
|
}
|
|
531
426
|
|
|
532
427
|
try {
|
|
533
|
-
await this.git.run({ cwd: repoPath, configs: gitConfigsPull, ...runOptions }, ...params);
|
|
428
|
+
await this.git.run({ cwd: repoPath, configs: gitConfigsPull, errors: 'throw', ...runOptions }, ...params);
|
|
534
429
|
} catch (ex) {
|
|
535
430
|
await this.throwIfSigningError(repoPath, params, ex, options.source);
|
|
536
431
|
throw getGitCommandError(
|
|
@@ -574,12 +469,17 @@ export class OperationsGitSubProvider implements GitOperationsSubProvider {
|
|
|
574
469
|
const branch = await this.provider.branches.getBranch(repoPath);
|
|
575
470
|
if (branch == null) return;
|
|
576
471
|
|
|
577
|
-
|
|
578
|
-
options
|
|
579
|
-
?
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
472
|
+
if (options?.reference != null) {
|
|
473
|
+
branchName = `${options.reference.ref}:${
|
|
474
|
+
options?.publish != null ? 'refs/heads/' : ''
|
|
475
|
+
}${branch.nameWithoutRemote}`;
|
|
476
|
+
} else {
|
|
477
|
+
const tracking = options?.publish == null ? branch.trackingWithoutRemote : undefined;
|
|
478
|
+
branchName =
|
|
479
|
+
tracking != null && tracking !== branch.nameWithoutRemote
|
|
480
|
+
? `${branch.name}:${tracking}`
|
|
481
|
+
: branch.name;
|
|
482
|
+
}
|
|
583
483
|
remoteName = branch.remoteName ?? options?.publish?.remote;
|
|
584
484
|
upstreamName = options?.reference == null && options?.publish != null ? branch.name : undefined;
|
|
585
485
|
|
|
@@ -599,10 +499,12 @@ export class OperationsGitSubProvider implements GitOperationsSubProvider {
|
|
|
599
499
|
|
|
600
500
|
let forceOpts: PushForceOptions | undefined;
|
|
601
501
|
if (options?.force) {
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
502
|
+
// Honor the host's force-push preferences (e.g. VS Code's `git.useForcePushWithLease` /
|
|
503
|
+
// `git.useForcePushIfIncludes`); fall back to the safer `--force-with-lease` behavior.
|
|
504
|
+
const useForceWithLease = this.context.config?.push?.useForceWithLease ?? true;
|
|
505
|
+
forceOpts = useForceWithLease
|
|
506
|
+
? { withLease: true, ifIncludes: this.context.config?.push?.useForceIfIncludes ?? true }
|
|
507
|
+
: { withLease: false };
|
|
606
508
|
}
|
|
607
509
|
|
|
608
510
|
try {
|
|
@@ -688,7 +590,7 @@ export class OperationsGitSubProvider implements GitOperationsSubProvider {
|
|
|
688
590
|
}
|
|
689
591
|
|
|
690
592
|
try {
|
|
691
|
-
await this.git.run({ cwd: repoPath, ...runOptions }, ...params);
|
|
593
|
+
await this.git.run({ cwd: repoPath, errors: 'throw', ...runOptions }, ...params);
|
|
692
594
|
} catch (ex) {
|
|
693
595
|
const error = getGitCommandError(
|
|
694
596
|
'push',
|
|
@@ -758,6 +660,8 @@ export class OperationsGitSubProvider implements GitOperationsSubProvider {
|
|
|
758
660
|
branch?: string;
|
|
759
661
|
editor?: string;
|
|
760
662
|
interactive?: boolean;
|
|
663
|
+
programmaticEditor?: boolean;
|
|
664
|
+
messageEditor?: string;
|
|
761
665
|
onto?: string;
|
|
762
666
|
updateRefs?: boolean;
|
|
763
667
|
source?: unknown;
|
|
@@ -767,7 +671,7 @@ export class OperationsGitSubProvider implements GitOperationsSubProvider {
|
|
|
767
671
|
const scope = getScopedLogger();
|
|
768
672
|
|
|
769
673
|
const args = ['rebase'];
|
|
770
|
-
|
|
674
|
+
const configs: string[] = [];
|
|
771
675
|
|
|
772
676
|
if (options?.autoStash !== false) {
|
|
773
677
|
args.push('--autostash');
|
|
@@ -777,8 +681,24 @@ export class OperationsGitSubProvider implements GitOperationsSubProvider {
|
|
|
777
681
|
args.push('--interactive');
|
|
778
682
|
|
|
779
683
|
if (options.editor) {
|
|
780
|
-
configs
|
|
684
|
+
configs.push('-c', `sequence.editor=${options.editor}`);
|
|
781
685
|
}
|
|
686
|
+
|
|
687
|
+
// A script-based sequence editor rewrites the todo by command word + SHA, so git must emit a
|
|
688
|
+
// plain, natural-order todo regardless of the user's rebase config: `rebase.autosquash` would
|
|
689
|
+
// reorder commits and rewrite `pick`→`fixup`, and `rebase.abbreviateCommands` would emit `p`.
|
|
690
|
+
if (options.programmaticEditor) {
|
|
691
|
+
configs.push('-c', 'rebase.autosquash=false', '-c', 'rebase.abbreviateCommands=false');
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
// Drive per-commit message editing (the combined message a `squash` produces, or a `reword`).
|
|
696
|
+
// `GIT_EDITOR` (not `core.editor`) is what git's interactive-rebase backend honors for the `reword`
|
|
697
|
+
// amend step, so set it on the environment; also set `core.editor` config as a fallback.
|
|
698
|
+
let env = runOptions?.env;
|
|
699
|
+
if (options?.messageEditor) {
|
|
700
|
+
configs.push('-c', `core.editor=${options.messageEditor}`);
|
|
701
|
+
env = { ...env, GIT_EDITOR: options.messageEditor };
|
|
782
702
|
}
|
|
783
703
|
|
|
784
704
|
if (options?.updateRefs) {
|
|
@@ -798,7 +718,7 @@ export class OperationsGitSubProvider implements GitOperationsSubProvider {
|
|
|
798
718
|
try {
|
|
799
719
|
await this.git.run(
|
|
800
720
|
// Avoid a timeout since rebases can take a long time (set to 0 to disable)
|
|
801
|
-
{ cwd: repoPath, errors: 'throw', configs: configs, timeout: 0, ...runOptions },
|
|
721
|
+
{ cwd: repoPath, errors: 'throw', configs: configs, timeout: 0, ...runOptions, env: env },
|
|
802
722
|
...args,
|
|
803
723
|
);
|
|
804
724
|
this.context.hooks?.cache?.onReset?.(repoPath, 'branches', 'status');
|
|
@@ -859,7 +779,7 @@ export class OperationsGitSubProvider implements GitOperationsSubProvider {
|
|
|
859
779
|
params.push(rev, '--');
|
|
860
780
|
|
|
861
781
|
try {
|
|
862
|
-
await this.git.run({ cwd: repoPath, ...runOptions }, ...params);
|
|
782
|
+
await this.git.run({ cwd: repoPath, errors: 'throw', ...runOptions }, ...params);
|
|
863
783
|
} catch (ex) {
|
|
864
784
|
throw getGitCommandError(
|
|
865
785
|
'reset',
|
|
@@ -870,6 +790,74 @@ export class OperationsGitSubProvider implements GitOperationsSubProvider {
|
|
|
870
790
|
}
|
|
871
791
|
}
|
|
872
792
|
|
|
793
|
+
@sequentialize({ getQueueKey: rp => rp })
|
|
794
|
+
@debug()
|
|
795
|
+
async restore(
|
|
796
|
+
repoPath: string,
|
|
797
|
+
path: string | string[],
|
|
798
|
+
options?: { ref?: string; side?: 'ours' | 'theirs' },
|
|
799
|
+
runOptions?: GitOperationRunOptions,
|
|
800
|
+
): Promise<void> {
|
|
801
|
+
const paths = Array.isArray(path) ? path : [path];
|
|
802
|
+
if (!paths.length) return;
|
|
803
|
+
|
|
804
|
+
const scope = getScopedLogger();
|
|
805
|
+
const normalized = paths.map(p => splitPath(p, repoPath, true)[0]);
|
|
806
|
+
|
|
807
|
+
// `side` and `ref` are mutually exclusive in git (--ours/--theirs apply during conflict
|
|
808
|
+
// resolution and take no source ref); side takes precedence if both are somehow supplied.
|
|
809
|
+
// Use a truthy check on `ref` so an empty string falls through to index-restore rather than
|
|
810
|
+
// producing a malformed `git checkout '' -- …`. Implemented via `checkout` rather than
|
|
811
|
+
// `git restore` for compatibility with the minimum supported git (2.7.2); `git restore`
|
|
812
|
+
// only landed in 2.23.
|
|
813
|
+
const selector = options?.side != null ? `--${options.side}` : options?.ref || undefined;
|
|
814
|
+
const prefix = selector != null ? ['checkout', selector, '--'] : ['checkout', '--'];
|
|
815
|
+
|
|
816
|
+
try {
|
|
817
|
+
if (await this.git.supports('git:checkout:pathspec-from-file')) {
|
|
818
|
+
// Git ≥ 2.26: stream all pathspecs via stdin (NUL-separated) in a single invocation —
|
|
819
|
+
// no CLI-length limit (so no batching), and robust for paths with spaces/newlines.
|
|
820
|
+
// Pathspecs come from the file rather than argv, so `--` isn't used.
|
|
821
|
+
const args = ['checkout'];
|
|
822
|
+
if (selector != null) {
|
|
823
|
+
args.push(selector);
|
|
824
|
+
}
|
|
825
|
+
args.push('--pathspec-from-file=-', '--pathspec-file-nul');
|
|
826
|
+
await this.git.run(
|
|
827
|
+
{ cwd: repoPath, errors: 'throw', ...runOptions, stdin: normalized.join('\0') },
|
|
828
|
+
...args,
|
|
829
|
+
);
|
|
830
|
+
} else {
|
|
831
|
+
// Older git: pass pathspecs as args, one git invocation per chunk (collapsing N
|
|
832
|
+
// path-mode checkouts into ⌈N / chunk⌉ spawns). Reserve the command prefix length and
|
|
833
|
+
// a per-arg overhead (separator + possible quoting) so the assembled command line
|
|
834
|
+
// can't exceed the OS limit even with many short paths.
|
|
835
|
+
const perArgOverhead = 3;
|
|
836
|
+
const prefixLength = prefix.reduce((sum, arg) => sum + arg.length + perArgOverhead, 0);
|
|
837
|
+
for (const batch of chunkByStringLength(normalized, maxGitCliLength - prefixLength, perArgOverhead)) {
|
|
838
|
+
await this.git.run({ cwd: repoPath, errors: 'throw', ...runOptions }, ...prefix, ...batch);
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
this.context.hooks?.cache?.onReset?.(repoPath, 'status');
|
|
842
|
+
this.context.hooks?.repository?.onChanged?.(repoPath, ['index']);
|
|
843
|
+
} catch (ex) {
|
|
844
|
+
scope?.error(ex);
|
|
845
|
+
throw getGitCommandError(
|
|
846
|
+
'checkout',
|
|
847
|
+
ex,
|
|
848
|
+
reason =>
|
|
849
|
+
new CheckoutError(
|
|
850
|
+
{
|
|
851
|
+
reason: reason ?? 'other',
|
|
852
|
+
ref: options?.side ?? options?.ref,
|
|
853
|
+
gitCommand: { repoPath: repoPath, args: [...prefix, ...normalized] },
|
|
854
|
+
},
|
|
855
|
+
ex,
|
|
856
|
+
),
|
|
857
|
+
);
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
|
|
873
861
|
@sequentialize({ getQueueKey: rp => rp })
|
|
874
862
|
@debug()
|
|
875
863
|
async revert(
|
|
@@ -390,7 +390,25 @@ export class StashGitSubProvider implements GitStashSubProvider {
|
|
|
390
390
|
params.push('--include-untracked');
|
|
391
391
|
}
|
|
392
392
|
|
|
393
|
-
|
|
393
|
+
// `git stash push --keep-index --include-untracked -- <pathspec>` hits a bug in git when the
|
|
394
|
+
// pathspec names an untracked file: the restoring checkout can't match an untracked path in the
|
|
395
|
+
// index tree, so it fails and the staged changes are lost.
|
|
396
|
+
//
|
|
397
|
+
// $ mkdir stash-test && cd stash-test && git init
|
|
398
|
+
// $ echo a > a.txt
|
|
399
|
+
// $ git add a.txt
|
|
400
|
+
// $ git commit -m init
|
|
401
|
+
// $ echo b > b.txt
|
|
402
|
+
// $ git stash push --keep-index --include-untracked -- b.txt
|
|
403
|
+
// Saved working directory and index state WIP on main: 8a280fe init
|
|
404
|
+
// error: pathspec ':(prefix:0)b.txt' did not match any file(s) known to git
|
|
405
|
+
//
|
|
406
|
+
// Only suppress --keep-index for that case — when the selection actually includes untracked
|
|
407
|
+
// files (`includeUntracked`) AND we're stashing by pathspec. Keying off `includeUntracked` (the
|
|
408
|
+
// caller's intent) rather than the `--include-untracked` flag is deliberate: we add
|
|
409
|
+
// `--include-untracked` to every pathspec push above as a safety net, but a tracked-only
|
|
410
|
+
// selection can — and must — still keep the index intact.
|
|
411
|
+
if (options?.keepIndex && !(options?.includeUntracked && options?.pathspecs?.length)) {
|
|
394
412
|
params.push('--keep-index');
|
|
395
413
|
}
|
|
396
414
|
|
|
@@ -494,7 +512,7 @@ export function convertStashesToStdin(stashOrStashes: GitStash | ReadonlyMap<str
|
|
|
494
512
|
}
|
|
495
513
|
|
|
496
514
|
const stashSummaryRegex =
|
|
497
|
-
//
|
|
515
|
+
// oxlint-disable-next-line no-control-regex
|
|
498
516
|
/(?:(?:(?<wip>WIP) on|On) (?<onref>[^/](?!.*\/\.)(?!.*\.\.)(?!.*\/\/)(?!.*@\{)[^\x00-\x1F\x7F ~^:?*[\\]+[^./]):\s*)?(?<summary>.*)$/s;
|
|
499
517
|
|
|
500
518
|
function createStash(
|