@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
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { base64, fromBase64 } from '../../utils/base64.js';
|
|
2
|
+
import type { GraphReachabilityTable } from '../models/graph.js';
|
|
3
|
+
import type { GitCommitReachability } from '../providers/commits.js';
|
|
4
|
+
|
|
5
|
+
type ReachableRef = GitCommitReachability['refs'][number];
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Canonical dictionary key for a reachable ref. The `r`/`l` discriminator keeps a remote branch from
|
|
9
|
+
* colliding with a local branch of the same name; the `refType` prefix separates tags from branches.
|
|
10
|
+
* Single-sourced here so the encoder ({@link createReachabilityTableBuilder}) and any future consumer
|
|
11
|
+
* can't drift on the convention.
|
|
12
|
+
*/
|
|
13
|
+
export function reachableRefKey(ref: ReachableRef): string {
|
|
14
|
+
return `${ref.refType}:${ref.refType === 'branch' && ref.remote ? 'r' : 'l'}:${ref.name}`;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Monotonic generation counter — each builder (i.e. each fresh graph walk) gets a distinct id, while a
|
|
18
|
+
// builder reused across `more()` pagination keeps its id. Only ever incremented host-side (the encoder);
|
|
19
|
+
// the webview treats the id as opaque. Module-scoped so ids are unique across a session's builders.
|
|
20
|
+
let nextReachabilityTableId = 0;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Builds a {@link GraphReachabilityTable} incrementally: interns refs into a shared dictionary and
|
|
24
|
+
* distinct membership bitmaps into a deduplicated set list. Kept next to {@link decodeReachabilitySet}
|
|
25
|
+
* (its exact inverse) so the wire format — first-seen dictionary indexing, LSB-first bit packing,
|
|
26
|
+
* base64 — has a single owner and a round-trippable test, rather than an encoder and decoder that can
|
|
27
|
+
* silently diverge across packages.
|
|
28
|
+
*
|
|
29
|
+
* Append-only by construction: an index, once assigned, is never reused — so a builder kept alive
|
|
30
|
+
* across paginated loads accumulates a table whose existing indices stay valid as it grows.
|
|
31
|
+
*/
|
|
32
|
+
export function createReachabilityTableBuilder(): {
|
|
33
|
+
intern: (refs: Iterable<ReachableRef> | undefined) => number | undefined;
|
|
34
|
+
build: () => GraphReachabilityTable | undefined;
|
|
35
|
+
} {
|
|
36
|
+
const id = ++nextReachabilityTableId;
|
|
37
|
+
const dictionary: GitCommitReachability['refs'] = [];
|
|
38
|
+
const refKeyToIndex = new Map<string, number>();
|
|
39
|
+
const sets: string[] = [];
|
|
40
|
+
const setKeyToIndex = new Map<string, number>();
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
/** Interns a ref set, returning its index into `sets`, or undefined when the set is empty. */
|
|
44
|
+
intern: (refs: Iterable<ReachableRef> | undefined): number | undefined => {
|
|
45
|
+
if (refs == null) return undefined;
|
|
46
|
+
|
|
47
|
+
let maxIndex = -1;
|
|
48
|
+
const indices: number[] = [];
|
|
49
|
+
for (const ref of refs) {
|
|
50
|
+
const key = reachableRefKey(ref);
|
|
51
|
+
let index = refKeyToIndex.get(key);
|
|
52
|
+
if (index == null) {
|
|
53
|
+
index = dictionary.length;
|
|
54
|
+
refKeyToIndex.set(key, index);
|
|
55
|
+
dictionary.push(ref);
|
|
56
|
+
}
|
|
57
|
+
indices.push(index);
|
|
58
|
+
if (index > maxIndex) {
|
|
59
|
+
maxIndex = index;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
if (indices.length === 0) return undefined;
|
|
63
|
+
|
|
64
|
+
// Pack into a bitset sized to this set's high-water dictionary index, then intern by base64.
|
|
65
|
+
const bytes = new Uint8Array((maxIndex >> 3) + 1);
|
|
66
|
+
for (const index of indices) {
|
|
67
|
+
bytes[index >> 3] |= 1 << (index & 7);
|
|
68
|
+
}
|
|
69
|
+
const encoded = base64(bytes);
|
|
70
|
+
let setIndex = setKeyToIndex.get(encoded);
|
|
71
|
+
if (setIndex == null) {
|
|
72
|
+
setIndex = sets.length;
|
|
73
|
+
setKeyToIndex.set(encoded, setIndex);
|
|
74
|
+
sets.push(encoded);
|
|
75
|
+
}
|
|
76
|
+
return setIndex;
|
|
77
|
+
},
|
|
78
|
+
|
|
79
|
+
/** The accumulated table, or undefined when nothing was interned. */
|
|
80
|
+
build: (): GraphReachabilityTable | undefined =>
|
|
81
|
+
dictionary.length ? { id: id, dictionary: dictionary, sets: sets } : undefined,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Decodes the membership bitmap at `index` in a {@link GraphReachabilityTable} back into its list of
|
|
87
|
+
* reachable refs. Refs come back in the table's first-seen dictionary order — callers that need the
|
|
88
|
+
* canonical display order (current-first / local-before-remote / tags newest-first) sort the result
|
|
89
|
+
* with `compareReachableRefs`. Returns an empty array for an out-of-range/missing set index.
|
|
90
|
+
*
|
|
91
|
+
* The bitmap is at most `ceil(dictionary.length / 8)` bytes; earlier (shorter) bitmaps packed against
|
|
92
|
+
* a smaller dictionary stay valid — only bits actually present can be set, so the loop stops at the
|
|
93
|
+
* shorter of the dictionary length and the bitmap's bit count rather than scanning a guaranteed-zero tail.
|
|
94
|
+
*/
|
|
95
|
+
export function decodeReachabilitySet(table: GraphReachabilityTable, index: number): GitCommitReachability['refs'] {
|
|
96
|
+
const { dictionary, sets } = table;
|
|
97
|
+
const encoded = sets[index];
|
|
98
|
+
if (encoded == null) return [];
|
|
99
|
+
|
|
100
|
+
const bytes = fromBase64(encoded);
|
|
101
|
+
const refs: GitCommitReachability['refs'] = [];
|
|
102
|
+
const count = Math.min(dictionary.length, bytes.length * 8);
|
|
103
|
+
for (let i = 0; i < count; i++) {
|
|
104
|
+
if ((bytes[i >> 3] ?? 0) & (1 << (i & 7))) {
|
|
105
|
+
refs.push(dictionary[i]);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return refs;
|
|
109
|
+
}
|
|
@@ -43,7 +43,7 @@ interface SessionRecord {
|
|
|
43
43
|
* implementations stored just the first caller's hooks (`WatchHooks | undefined`) and
|
|
44
44
|
* silently dropped subsequent callers' hooks — that broke the case where a low-level
|
|
45
45
|
* subscriber (e.g. the Graph's secondary-worktree WIP watcher) opened the session
|
|
46
|
-
* first without hooks, and a later `Repository.
|
|
46
|
+
* first without hooks, and a later `Repository.watch()` for the same path lost
|
|
47
47
|
* its `markFetched` / `onIgnoresChanged` wiring. Each `watch()` call adds its hooks
|
|
48
48
|
* to this set and removes them on dispose; fire sites iterate the set.
|
|
49
49
|
*/
|
|
@@ -78,6 +78,8 @@ export class RepositoryWatchService implements UnifiedDisposable {
|
|
|
78
78
|
private readonly watchGroups = new Map<string, WatchGroup>();
|
|
79
79
|
private readonly wtWatchers = new Map<string, { watcher: FileWatcher; filter?: GitIgnoreFilter }>();
|
|
80
80
|
private _disposed = false;
|
|
81
|
+
/** Global suspend state (driven by window focus); inherited by sessions created via {@link watch}. */
|
|
82
|
+
private _suspended = false;
|
|
81
83
|
|
|
82
84
|
constructor(options: WatchServiceOptions) {
|
|
83
85
|
this.provider = options.watchingProvider;
|
|
@@ -135,6 +137,7 @@ export class RepositoryWatchService implements UnifiedDisposable {
|
|
|
135
137
|
repoPath: repoPath,
|
|
136
138
|
defaultRepoDelayMs: this.defaultRepoDelayMs,
|
|
137
139
|
defaultWorkingTreeDelayMs: this.defaultWorkingTreeDelayMs,
|
|
140
|
+
suspended: this._suspended,
|
|
138
141
|
lifecycle: {
|
|
139
142
|
onFirstRepoSubscriber: (): void => {
|
|
140
143
|
const rec = this.sessionMap.get(repoPath);
|
|
@@ -207,15 +210,17 @@ export class RepositoryWatchService implements UnifiedDisposable {
|
|
|
207
210
|
return this.sessionMap.get(repoPath)?.session;
|
|
208
211
|
}
|
|
209
212
|
|
|
210
|
-
/** Suspend all sessions */
|
|
213
|
+
/** Suspend all sessions and mark the service suspended so late-joining sessions start suspended */
|
|
211
214
|
suspendAll(): void {
|
|
215
|
+
this._suspended = true;
|
|
212
216
|
for (const record of this.sessionMap.values()) {
|
|
213
217
|
record.session.suspend();
|
|
214
218
|
}
|
|
215
219
|
}
|
|
216
220
|
|
|
217
|
-
/** Resume all sessions, with optional per-session delay */
|
|
221
|
+
/** Resume all sessions (clearing the global suspend state), with optional per-session delay */
|
|
218
222
|
resumeAll(getDelay?: (session: RepositoryWatchSession) => number): void {
|
|
223
|
+
this._suspended = false;
|
|
219
224
|
for (const record of this.sessionMap.values()) {
|
|
220
225
|
const delayMs = getDelay?.(record.session);
|
|
221
226
|
record.session.resume(delayMs);
|
|
@@ -252,7 +257,7 @@ export class RepositoryWatchService implements UnifiedDisposable {
|
|
|
252
257
|
},
|
|
253
258
|
onFetchHeadChanged: function (repoPath: string): void {
|
|
254
259
|
// Iterate at fire time so callers that joined after the group was wired up
|
|
255
|
-
// still receive events (e.g. a `Repository.
|
|
260
|
+
// still receive events (e.g. a `Repository.watch()` whose hooks landed
|
|
256
261
|
// after a hookless `GitRepositoryService.watch` registered first).
|
|
257
262
|
for (const h of watchHooks) {
|
|
258
263
|
h.onFetchHeadChanged?.(repoPath);
|
|
@@ -37,6 +37,8 @@ export interface WatchSessionOptions {
|
|
|
37
37
|
/** Default debounce for working tree changes. Default: 2500ms */
|
|
38
38
|
readonly defaultWorkingTreeDelayMs?: number;
|
|
39
39
|
readonly lifecycle?: WatchSessionLifecycle;
|
|
40
|
+
/** Initial suspended state — new sessions inherit the service-global suspend state. Default: false */
|
|
41
|
+
readonly suspended?: boolean;
|
|
40
42
|
/**
|
|
41
43
|
* Called whenever a debounced repo change event is dispatched to subscribers.
|
|
42
44
|
* Does NOT count as a subscriber — used by WatchService for
|
|
@@ -66,16 +68,16 @@ export class RepositoryWatchSession implements UnifiedDisposable {
|
|
|
66
68
|
private effectiveRepoMs: number;
|
|
67
69
|
private readonly defaultRepoMs: number;
|
|
68
70
|
|
|
69
|
-
private readonly
|
|
70
|
-
private readonly
|
|
71
|
-
private
|
|
72
|
-
private
|
|
73
|
-
private
|
|
74
|
-
private
|
|
75
|
-
private readonly
|
|
71
|
+
private readonly workingTreeEmitter = new Emitter<WorkingTreeChangeEvent>();
|
|
72
|
+
private readonly workingTreeDelays = new Map<number, number>();
|
|
73
|
+
private workingTreeNextId = 0;
|
|
74
|
+
private pendingWorkingTreePaths = new Set<string>();
|
|
75
|
+
private workingTreeTimer: ReturnType<typeof setTimeout> | undefined;
|
|
76
|
+
private effectiveWorkingTreeMs: number;
|
|
77
|
+
private readonly defaultWorkingTreeMs: number;
|
|
76
78
|
|
|
77
79
|
private _etagWorkingTree = 0;
|
|
78
|
-
private _suspended
|
|
80
|
+
private _suspended: boolean;
|
|
79
81
|
private _disposed = false;
|
|
80
82
|
|
|
81
83
|
private readonly lifecycle?: WatchSessionLifecycle;
|
|
@@ -84,11 +86,12 @@ export class RepositoryWatchSession implements UnifiedDisposable {
|
|
|
84
86
|
constructor(options: WatchSessionOptions) {
|
|
85
87
|
this.repoPath = options.repoPath;
|
|
86
88
|
this.defaultRepoMs = options.defaultRepoDelayMs ?? defaultRepoDelay;
|
|
87
|
-
this.
|
|
89
|
+
this.defaultWorkingTreeMs = options.defaultWorkingTreeDelayMs ?? defaultWorkingTreeDelay;
|
|
88
90
|
this.effectiveRepoMs = this.defaultRepoMs;
|
|
89
|
-
this.
|
|
91
|
+
this.effectiveWorkingTreeMs = this.defaultWorkingTreeMs;
|
|
90
92
|
this.lifecycle = options.lifecycle;
|
|
91
93
|
this.onDidFireRepoChangeCallback = options.onDidFireRepoChange;
|
|
94
|
+
this._suspended = options.suspended ?? false;
|
|
92
95
|
}
|
|
93
96
|
|
|
94
97
|
[Symbol.dispose](): void {
|
|
@@ -101,15 +104,15 @@ export class RepositoryWatchSession implements UnifiedDisposable {
|
|
|
101
104
|
this._disposed = true;
|
|
102
105
|
|
|
103
106
|
this.cancelRepoTimer();
|
|
104
|
-
this.
|
|
107
|
+
this.cancelWorkingTreeTimer();
|
|
105
108
|
|
|
106
109
|
this.pendingRepoEvent = undefined;
|
|
107
|
-
this.
|
|
110
|
+
this.pendingWorkingTreePaths.clear();
|
|
108
111
|
this.repoDelays.clear();
|
|
109
|
-
this.
|
|
112
|
+
this.workingTreeDelays.clear();
|
|
110
113
|
|
|
111
114
|
this.repoEmitter.dispose();
|
|
112
|
-
this.
|
|
115
|
+
this.workingTreeEmitter.dispose();
|
|
113
116
|
}
|
|
114
117
|
|
|
115
118
|
get suspended(): boolean {
|
|
@@ -117,7 +120,7 @@ export class RepositoryWatchSession implements UnifiedDisposable {
|
|
|
117
120
|
}
|
|
118
121
|
|
|
119
122
|
get hasPendingChanges(): boolean {
|
|
120
|
-
return this.pendingRepoEvent != null || this.
|
|
123
|
+
return this.pendingRepoEvent != null || this.pendingWorkingTreePaths.size > 0;
|
|
121
124
|
}
|
|
122
125
|
|
|
123
126
|
get repoSubscriberCount(): number {
|
|
@@ -125,7 +128,7 @@ export class RepositoryWatchSession implements UnifiedDisposable {
|
|
|
125
128
|
}
|
|
126
129
|
|
|
127
130
|
get workingTreeSubscriberCount(): number {
|
|
128
|
-
return this.
|
|
131
|
+
return this.workingTreeDelays.size;
|
|
129
132
|
}
|
|
130
133
|
|
|
131
134
|
/**
|
|
@@ -197,26 +200,26 @@ export class RepositoryWatchSession implements UnifiedDisposable {
|
|
|
197
200
|
return Object.assign(
|
|
198
201
|
createDisposable(() => {}),
|
|
199
202
|
{
|
|
200
|
-
onDidChangeWorkingTree: this.
|
|
203
|
+
onDidChangeWorkingTree: this.workingTreeEmitter.event,
|
|
201
204
|
},
|
|
202
205
|
);
|
|
203
206
|
}
|
|
204
207
|
|
|
205
|
-
const id = this.
|
|
206
|
-
const delay = opts?.delayMs ?? this.
|
|
207
|
-
const wasEmpty = this.
|
|
208
|
-
const prevDelay = this.
|
|
208
|
+
const id = this.workingTreeNextId++;
|
|
209
|
+
const delay = opts?.delayMs ?? this.defaultWorkingTreeMs;
|
|
210
|
+
const wasEmpty = this.workingTreeDelays.size === 0;
|
|
211
|
+
const prevDelay = this.effectiveWorkingTreeMs;
|
|
209
212
|
|
|
210
|
-
this.
|
|
211
|
-
this.
|
|
213
|
+
this.workingTreeDelays.set(id, delay);
|
|
214
|
+
this.recalcWorkingTreeDelay();
|
|
212
215
|
|
|
213
216
|
if (wasEmpty) {
|
|
214
217
|
this.lifecycle?.onFirstWorkingTreeSubscriber?.();
|
|
215
218
|
}
|
|
216
219
|
|
|
217
220
|
// If the effective delay got shorter while a timer is running, reschedule sooner
|
|
218
|
-
if (this.
|
|
219
|
-
this.
|
|
221
|
+
if (this.effectiveWorkingTreeMs < prevDelay && this.workingTreeTimer != null) {
|
|
222
|
+
this.scheduleWorkingTreeFlush();
|
|
220
223
|
}
|
|
221
224
|
|
|
222
225
|
let subDisposed = false;
|
|
@@ -226,16 +229,16 @@ export class RepositoryWatchSession implements UnifiedDisposable {
|
|
|
226
229
|
|
|
227
230
|
subDisposed = true;
|
|
228
231
|
|
|
229
|
-
this.
|
|
230
|
-
this.
|
|
232
|
+
this.workingTreeDelays.delete(id);
|
|
233
|
+
this.recalcWorkingTreeDelay();
|
|
231
234
|
|
|
232
|
-
if (this.
|
|
233
|
-
this.
|
|
234
|
-
this.
|
|
235
|
+
if (this.workingTreeDelays.size === 0) {
|
|
236
|
+
this.cancelWorkingTreeTimer();
|
|
237
|
+
this.pendingWorkingTreePaths.clear();
|
|
235
238
|
this.lifecycle?.onLastWorkingTreeSubscriber?.();
|
|
236
239
|
}
|
|
237
240
|
}),
|
|
238
|
-
{ onDidChangeWorkingTree: this.
|
|
241
|
+
{ onDidChangeWorkingTree: this.workingTreeEmitter.event },
|
|
239
242
|
);
|
|
240
243
|
}
|
|
241
244
|
|
|
@@ -243,7 +246,7 @@ export class RepositoryWatchSession implements UnifiedDisposable {
|
|
|
243
246
|
this._suspended = true;
|
|
244
247
|
// Cancel active timers — events continue to coalesce but don't fire
|
|
245
248
|
this.cancelRepoTimer();
|
|
246
|
-
this.
|
|
249
|
+
this.cancelWorkingTreeTimer();
|
|
247
250
|
}
|
|
248
251
|
|
|
249
252
|
resume(delayMs?: number): void {
|
|
@@ -255,8 +258,8 @@ export class RepositoryWatchSession implements UnifiedDisposable {
|
|
|
255
258
|
if (this.pendingRepoEvent != null) {
|
|
256
259
|
this.scheduleRepoFlush(delayMs);
|
|
257
260
|
}
|
|
258
|
-
if (this.
|
|
259
|
-
this.
|
|
261
|
+
if (this.pendingWorkingTreePaths.size > 0) {
|
|
262
|
+
this.scheduleWorkingTreeFlush(delayMs);
|
|
260
263
|
}
|
|
261
264
|
}
|
|
262
265
|
|
|
@@ -286,7 +289,7 @@ export class RepositoryWatchSession implements UnifiedDisposable {
|
|
|
286
289
|
if (this._disposed) return;
|
|
287
290
|
|
|
288
291
|
this._etagWorkingTree++;
|
|
289
|
-
this.
|
|
292
|
+
this.coalesceWorkingTrees(paths);
|
|
290
293
|
}
|
|
291
294
|
|
|
292
295
|
private static minDelay(delays: Map<number, number>, fallback: number): number {
|
|
@@ -305,8 +308,11 @@ export class RepositoryWatchSession implements UnifiedDisposable {
|
|
|
305
308
|
this.effectiveRepoMs = RepositoryWatchSession.minDelay(this.repoDelays, this.defaultRepoMs);
|
|
306
309
|
}
|
|
307
310
|
|
|
308
|
-
private
|
|
309
|
-
this.
|
|
311
|
+
private recalcWorkingTreeDelay(): void {
|
|
312
|
+
this.effectiveWorkingTreeMs = RepositoryWatchSession.minDelay(
|
|
313
|
+
this.workingTreeDelays,
|
|
314
|
+
this.defaultWorkingTreeMs,
|
|
315
|
+
);
|
|
310
316
|
}
|
|
311
317
|
|
|
312
318
|
private flushRepo(): void {
|
|
@@ -339,29 +345,29 @@ export class RepositoryWatchSession implements UnifiedDisposable {
|
|
|
339
345
|
this.scheduleRepoFlush();
|
|
340
346
|
}
|
|
341
347
|
|
|
342
|
-
private
|
|
343
|
-
this.
|
|
344
|
-
if (this.
|
|
348
|
+
private flushWorkingTree(): void {
|
|
349
|
+
this.workingTreeTimer = undefined;
|
|
350
|
+
if (this.pendingWorkingTreePaths.size === 0) return;
|
|
345
351
|
|
|
346
|
-
const paths = this.
|
|
347
|
-
this.
|
|
348
|
-
this.
|
|
352
|
+
const paths = this.pendingWorkingTreePaths;
|
|
353
|
+
this.pendingWorkingTreePaths = new Set();
|
|
354
|
+
this.workingTreeEmitter.fire({ repoPath: this.repoPath, paths: paths });
|
|
349
355
|
}
|
|
350
356
|
|
|
351
|
-
private
|
|
352
|
-
if (this._suspended || this.
|
|
357
|
+
private scheduleWorkingTreeFlush(delayOverride?: number): void {
|
|
358
|
+
if (this._suspended || this.pendingWorkingTreePaths.size === 0) return;
|
|
353
359
|
|
|
354
|
-
if (this.
|
|
355
|
-
clearTimeout(this.
|
|
360
|
+
if (this.workingTreeTimer != null) {
|
|
361
|
+
clearTimeout(this.workingTreeTimer);
|
|
356
362
|
}
|
|
357
|
-
this.
|
|
363
|
+
this.workingTreeTimer = setTimeout(() => this.flushWorkingTree(), delayOverride ?? this.effectiveWorkingTreeMs);
|
|
358
364
|
}
|
|
359
365
|
|
|
360
|
-
private
|
|
366
|
+
private coalesceWorkingTrees(paths: Iterable<string>): void {
|
|
361
367
|
for (const p of paths) {
|
|
362
|
-
this.
|
|
368
|
+
this.pendingWorkingTreePaths.add(p);
|
|
363
369
|
}
|
|
364
|
-
this.
|
|
370
|
+
this.scheduleWorkingTreeFlush();
|
|
365
371
|
}
|
|
366
372
|
|
|
367
373
|
private cancelRepoTimer(): void {
|
|
@@ -371,10 +377,10 @@ export class RepositoryWatchSession implements UnifiedDisposable {
|
|
|
371
377
|
}
|
|
372
378
|
}
|
|
373
379
|
|
|
374
|
-
private
|
|
375
|
-
if (this.
|
|
376
|
-
clearTimeout(this.
|
|
377
|
-
this.
|
|
380
|
+
private cancelWorkingTreeTimer(): void {
|
|
381
|
+
if (this.workingTreeTimer != null) {
|
|
382
|
+
clearTimeout(this.workingTreeTimer);
|
|
383
|
+
this.workingTreeTimer = undefined;
|
|
378
384
|
}
|
|
379
385
|
}
|
|
380
386
|
}
|
|
@@ -232,6 +232,7 @@ export class BranchesGitSubProvider implements GitBranchesSubProvider {
|
|
|
232
232
|
// Pre-populate current branch cache for worktrees based on worktreePath info
|
|
233
233
|
// This eliminates the need for rev-parse calls during branch reconstruction
|
|
234
234
|
for (const branch of branches) {
|
|
235
|
+
// oxlint-disable-next-line typescript/prefer-optional-chain
|
|
235
236
|
if (branch.worktree && branch.worktree.path) {
|
|
236
237
|
const { name } = parseRefName(branch.refName);
|
|
237
238
|
const reference = createReference(name, branch.worktree.path, {
|
|
@@ -129,6 +129,30 @@ export class CommitsGitSubProvider implements GitCommitsSubProvider {
|
|
|
129
129
|
return isNaN(count) ? undefined : count;
|
|
130
130
|
}
|
|
131
131
|
|
|
132
|
+
@debug({ exit: true })
|
|
133
|
+
async hasUnpublishedCommits(
|
|
134
|
+
repoPath: string,
|
|
135
|
+
rev: string,
|
|
136
|
+
cancellation?: AbortSignal,
|
|
137
|
+
): Promise<boolean | undefined> {
|
|
138
|
+
// `<rev> --not --remotes` = commits reachable from `rev` but not from any remote-tracking ref.
|
|
139
|
+
// `--max-count=1` early-exits at the first such commit, so this is a presence probe, not a count.
|
|
140
|
+
const result = await this.git.run(
|
|
141
|
+
{ cwd: repoPath, cancellation: cancellation, errors: 'ignore' },
|
|
142
|
+
'rev-list',
|
|
143
|
+
'--max-count=1',
|
|
144
|
+
rev,
|
|
145
|
+
'--not',
|
|
146
|
+
'--remotes',
|
|
147
|
+
'--',
|
|
148
|
+
);
|
|
149
|
+
if (result.cancelled || cancellation?.aborted) {
|
|
150
|
+
throw new CancellationError();
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return result.stdout.trim().length > 0;
|
|
154
|
+
}
|
|
155
|
+
|
|
132
156
|
@debug({ exit: true })
|
|
133
157
|
async getCommitDates(
|
|
134
158
|
repoPath: string,
|
|
@@ -27,17 +27,24 @@ import type { Git } from '../exec/git.js';
|
|
|
27
27
|
const mappedAuthorRegex = /(.+)\s<(.+)>/;
|
|
28
28
|
const emptyArray: readonly never[] = Object.freeze([]);
|
|
29
29
|
|
|
30
|
+
/** Catch-all pattern matching every key in `.git/gk/config` for the one-shot bulk read. */
|
|
31
|
+
const gkConfigAllPattern = '.';
|
|
32
|
+
|
|
30
33
|
/**
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
+
* Canonicalizes a git config key for lookup against `--get-regexp` output, which git emits with the
|
|
35
|
+
* section and variable-name lowercased and only the subsection case-preserved. Without this a
|
|
36
|
+
* subsection-less key like `gk.defaultRemote` would miss the lowercased `gk.defaultremote` in the map.
|
|
34
37
|
*/
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
38
|
+
function canonicalizeGitConfigKey(key: string): string {
|
|
39
|
+
const first = key.indexOf('.');
|
|
40
|
+
if (first === -1) return key.toLowerCase();
|
|
41
|
+
|
|
42
|
+
const last = key.lastIndexOf('.');
|
|
43
|
+
// `section.name` (no subsection) → fully lowercased; `section.subsection.name` → lowercase
|
|
44
|
+
// section + name, preserve the case-sensitive subsection in the middle.
|
|
45
|
+
if (first === last) return key.toLowerCase();
|
|
46
|
+
return `${key.slice(0, first).toLowerCase()}${key.slice(first, last)}${key.slice(last).toLowerCase()}`;
|
|
47
|
+
}
|
|
41
48
|
|
|
42
49
|
/**
|
|
43
50
|
* Parses git config --get-regex output into a Map.
|
|
@@ -309,45 +316,49 @@ export class ConfigGitSubProvider implements GitConfigSubProvider {
|
|
|
309
316
|
}
|
|
310
317
|
|
|
311
318
|
@debug()
|
|
312
|
-
async getGkConfig(
|
|
313
|
-
|
|
314
|
-
key
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
await this.
|
|
319
|
+
async getGkConfig(repoPath: string, key: GkConfigKeys | DeprecatedGkConfigKeys): Promise<string | undefined> {
|
|
320
|
+
// Served in-memory from a single bulk read of the whole `.git/gk/config` (no gk read needs `--type`).
|
|
321
|
+
// Canonicalize the key the way git does for `--get-regexp` output so subsection-less keys
|
|
322
|
+
// (e.g. `gk.defaultRemote`) match, and coerce an empty value to undefined to match the old
|
|
323
|
+
// `git config --get` (`.trim() || undefined`) contract.
|
|
324
|
+
return (await this.getGkConfigMap(repoPath)).get(canonicalizeGitConfigKey(key)) || undefined;
|
|
325
|
+
}
|
|
318
326
|
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
const set = gkConfigCacheableSets.find(s => s.match.test(key));
|
|
323
|
-
if (set != null) {
|
|
324
|
-
return this.cache.getGkConfig(repoPath, key, async () => {
|
|
325
|
-
const entries = await this.getGkConfigRegex(repoPath, set.pattern);
|
|
326
|
-
return entries.get(key);
|
|
327
|
-
});
|
|
328
|
-
}
|
|
327
|
+
@debug()
|
|
328
|
+
async getGkConfigRegex(repoPath: string, pattern: string): Promise<Map<string, string>> {
|
|
329
|
+
const all = await this.getGkConfigMap(repoPath);
|
|
329
330
|
|
|
330
|
-
|
|
331
|
-
|
|
331
|
+
// Filter the bulk map in-memory. The old path passed `pattern` to `git config --get-regex`
|
|
332
|
+
// (errors ignored → empty result on a bad pattern); preserve that no-throw contract here
|
|
333
|
+
// rather than letting an invalid-JS pattern reject the read.
|
|
334
|
+
let re: RegExp;
|
|
335
|
+
try {
|
|
336
|
+
re = new RegExp(pattern);
|
|
337
|
+
} catch {
|
|
338
|
+
return new Map();
|
|
339
|
+
}
|
|
332
340
|
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
return this.getConfigCore(repoPath, key, {
|
|
341
|
-
...options,
|
|
342
|
-
runGitLocally: true,
|
|
343
|
-
file: gkConfigPath,
|
|
344
|
-
});
|
|
341
|
+
const result = new Map<string, string>();
|
|
342
|
+
for (const [k, v] of all) {
|
|
343
|
+
if (re.test(k)) {
|
|
344
|
+
result.set(k, v);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
return result;
|
|
345
348
|
}
|
|
346
349
|
|
|
347
|
-
|
|
348
|
-
|
|
350
|
+
/**
|
|
351
|
+
* Reads the entire `.git/gk/config` in one `git config --get-regexp` and caches the parsed map
|
|
352
|
+
* per commonPath. Per-key (getGkConfig) and per-namespace (getGkConfigRegex) lookups are served
|
|
353
|
+
* from this single cached read — so a branch-overview render that fans out across several gk
|
|
354
|
+
* namespaces costs one `git config` call instead of one per namespace.
|
|
355
|
+
*/
|
|
356
|
+
private async getGkConfigMap(repoPath: string): Promise<Map<string, string>> {
|
|
357
|
+
// Migrate BEFORE populating the cache, not inside the factory: a first-time migration calls
|
|
358
|
+
// clearCaches('gkConfig'), which would otherwise evict the in-flight gkConfigMap entry the
|
|
359
|
+
// cache wrapper synchronously stored and force a redundant bulk re-read.
|
|
349
360
|
await this.migrateGkConfigFromGitConfig(this.cache.getCommonPath(repoPath));
|
|
350
|
-
return this.cache.
|
|
361
|
+
return this.cache.getGkConfigMap(repoPath, () => this.getGkConfigRegexCore(repoPath, gkConfigAllPattern));
|
|
351
362
|
}
|
|
352
363
|
|
|
353
364
|
private async getGkConfigRegexCore(repoPath: string, pattern: string): Promise<Map<string, string>> {
|
|
@@ -377,7 +388,9 @@ export class ConfigGitSubProvider implements GitConfigSubProvider {
|
|
|
377
388
|
|
|
378
389
|
const promise = this.setGkConfigCore(repoPath, key, value, options);
|
|
379
390
|
this._pendingGkConfigWrites.set(dedupKey, promise);
|
|
380
|
-
|
|
391
|
+
// Swallow the cleanup chain's rejection so a failed write doesn't surface as an unhandled
|
|
392
|
+
// rejection separate from the caller's own handling of the returned promise.
|
|
393
|
+
void promise.finally(() => this._pendingGkConfigWrites.delete(dedupKey)).catch(() => {});
|
|
381
394
|
return promise;
|
|
382
395
|
}
|
|
383
396
|
|
|
@@ -398,7 +411,9 @@ export class ConfigGitSubProvider implements GitConfigSubProvider {
|
|
|
398
411
|
|
|
399
412
|
await this.setConfigCore(repoPath, key, value, { file: gkConfigPath });
|
|
400
413
|
|
|
401
|
-
// Invalidate the
|
|
414
|
+
// Invalidate the bulk map (so the next read re-fetches) and the derived caches for this key's
|
|
415
|
+
// ref. The `.git/gk/config` file-watcher also fires `'gkConfig'` shortly after, which clears
|
|
416
|
+
// the same caches coarsely — both paths are idempotent.
|
|
402
417
|
this.cache.deleteGkConfig(repoPath, key, options);
|
|
403
418
|
}
|
|
404
419
|
|
|
@@ -418,11 +433,10 @@ export class ConfigGitSubProvider implements GitConfigSubProvider {
|
|
|
418
433
|
const sshProgram = configMap.get('gpg.ssh.program');
|
|
419
434
|
const allowedSignersFile = configMap.get('gpg.ssh.allowedsignersfile');
|
|
420
435
|
|
|
421
|
-
// Check if git config has commit signing enabled
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
// The library no longer reads a config for this.
|
|
436
|
+
// Check if git config has commit signing enabled, falling back to the host-level override
|
|
437
|
+
// (e.g. VS Code's `git.enableCommitSigning`) when `commit.gpgsign` is unset/false. The host
|
|
438
|
+
// override can only enable signing, never force it off.
|
|
439
|
+
const isEnabled = parseGitBoolean(enabledRaw) || this.context.config?.signing?.enabled === true;
|
|
426
440
|
|
|
427
441
|
return {
|
|
428
442
|
enabled: isEnabled,
|
|
@@ -218,7 +218,8 @@ export class DiffGitSubProvider implements GitDiffSubProvider {
|
|
|
218
218
|
},
|
|
219
219
|
): Promise<GitFile[] | undefined> {
|
|
220
220
|
try {
|
|
221
|
-
const similarityThreshold =
|
|
221
|
+
const similarityThreshold =
|
|
222
|
+
options?.similarityThreshold ?? this.context.config?.commits.similarityThreshold;
|
|
222
223
|
const configs =
|
|
223
224
|
options?.renameLimit != null
|
|
224
225
|
? [...gitConfigsDiff, '-c', `diff.renameLimit=${options.renameLimit}`]
|