@gitkraken/core-gitlens 0.10.7 → 0.11.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 +39 -1
- package/dist/git/cache.d.ts +34 -2
- package/dist/git/cache.js +55 -7
- package/dist/git/cache.js.map +1 -1
- package/dist/git/context.d.ts +1 -1
- package/dist/git/models/repository.d.ts +3 -2
- package/dist/git/models/repository.js +5 -4
- package/dist/git/models/repository.js.map +1 -1
- package/dist/git/providers/branches.d.ts +24 -0
- package/dist/git/providers/config.d.ts +3 -1
- package/dist/git/providers/provider.d.ts +13 -1
- package/dist/git/providers/staging.d.ts +1 -1
- package/dist/git/service.d.ts +17 -1
- package/dist/git/service.js +59 -8
- package/dist/git/service.js.map +1 -1
- package/dist/git/watching/changeEvent.d.ts +48 -5
- package/dist/git/watching/changeEvent.js +82 -7
- package/dist/git/watching/changeEvent.js.map +1 -1
- package/dist/git/watching/classifyChange.d.ts +13 -2
- package/dist/git/watching/classifyChange.js +40 -5
- package/dist/git/watching/classifyChange.js.map +1 -1
- package/dist/git/watching/gitIgnoreFilter.d.ts +57 -10
- package/dist/git/watching/gitIgnoreFilter.js +186 -29
- package/dist/git/watching/gitIgnoreFilter.js.map +1 -1
- package/dist/git/watching/initWatcher.d.ts +5 -1
- package/dist/git/watching/initWatcher.js +1 -1
- package/dist/git/watching/initWatcher.js.map +1 -1
- package/dist/git/watching/provider.d.ts +33 -8
- package/dist/git/watching/watchGroup.d.ts +13 -2
- package/dist/git/watching/watchGroup.js +60 -14
- package/dist/git/watching/watchGroup.js.map +1 -1
- package/dist/git/watching/watchService.d.ts +42 -1
- package/dist/git/watching/watchService.js +172 -20
- package/dist/git/watching/watchService.js.map +1 -1
- package/dist/git/watching/watchSession.d.ts +50 -17
- package/dist/git/watching/watchSession.js +274 -83
- package/dist/git/watching/watchSession.js.map +1 -1
- package/dist/git/watching/watcherPatterns.d.ts +13 -2
- package/dist/git/watching/watcherPatterns.js +22 -2
- package/dist/git/watching/watcherPatterns.js.map +1 -1
- package/dist/git/watching/worktreeSummary.d.ts +132 -0
- package/dist/git/watching/worktreeSummary.js +429 -0
- package/dist/git/watching/worktreeSummary.js.map +1 -0
- package/dist/git-cli/cliGitProvider.d.ts +6 -1
- package/dist/git-cli/cliGitProvider.js +87 -2
- package/dist/git-cli/cliGitProvider.js.map +1 -1
- package/dist/git-cli/providers/blame.js +1 -1
- package/dist/git-cli/providers/branches.d.ts +32 -1
- package/dist/git-cli/providers/branches.js +217 -0
- package/dist/git-cli/providers/branches.js.map +1 -1
- package/dist/git-cli/providers/commits.js +1 -1
- package/dist/git-cli/providers/config.d.ts +3 -0
- package/dist/git-cli/providers/config.js +12 -6
- package/dist/git-cli/providers/config.js.map +1 -1
- package/dist/git-cli/providers/contributors.js +1 -1
- package/dist/git-cli/providers/graph.js +1 -1
- package/dist/git-cli/providers/maintenance.js +1 -1
- package/dist/git-cli/providers/refs.js +1 -1
- package/dist/git-cli/providers/staging.js +1 -1
- package/dist/git-cli/providers/status.js +1 -1
- package/dist/git-cli/service.js +1 -0
- package/dist/git-cli/service.js.map +1 -1
- package/dist/ipc/ipcServer.js +1 -1
- package/dist/plus/agents/providers/gkAgentProvider.js +2 -2
- package/dist/plus/git-github/api/github.d.ts +1 -1
- package/dist/plus/git-github/api/github.js +1 -1
- package/dist/plus/git-github/providers/github/blame.js +1 -1
- package/dist/plus/integrations/authentication/integrationAuthenticationProvider.js +1 -1
- package/dist/plus/integrations/context.d.ts +1 -1
- package/dist/plus/integrations/integrationService.js +1 -1
- package/dist/plus/integrations/models/integration.js +1 -1
- package/dist/plus/integrations/providers/bitbucket-server.d.ts +1 -1
- package/dist/plus/integrations/reads/batchEtags.d.ts +5 -2
- package/dist/plus/integrations/reads/batchEtags.js +31 -10
- package/dist/plus/integrations/reads/batchEtags.js.map +1 -1
- package/dist/plus/integrations/reads/pullRequestBranches.js +10 -3
- package/dist/plus/integrations/reads/pullRequestBranches.js.map +1 -1
- package/dist/plus/integrations/results.d.ts +16 -1
- package/dist/plus/integrations/results.js +22 -3
- package/dist/plus/integrations/results.js.map +1 -1
- package/dist/utils/event.d.ts +1 -1
- package/docs/integrations.md +5 -0
- package/package.json +1 -1
|
@@ -0,0 +1,429 @@
|
|
|
1
|
+
import { areEqual } from "../../utils/array.js";
|
|
2
|
+
import { CancellationError, isCancellationError, raceWithSignal } from "../../utils/cancellation.js";
|
|
3
|
+
import { Logger as _logger } from "../../utils/logger.js";
|
|
4
|
+
import { Emitter } from "../../utils/event.js";
|
|
5
|
+
import { getRepositoryKey } from "../../utils/uri.js";
|
|
6
|
+
//#region ../git/src/watching/worktreeSummary.ts
|
|
7
|
+
/** Read groups, as bit flags so a pending set is a number rather than an allocation per event */
|
|
8
|
+
const dirtyField = 1;
|
|
9
|
+
const branchField = 2;
|
|
10
|
+
const baseField = 4;
|
|
11
|
+
/** Re-reads only the stored values the base read consults, and the base itself only when they changed */
|
|
12
|
+
const baseKeysField = 8;
|
|
13
|
+
/** Every group a full read covers — a base read refreshes the {@link BaseKeys} snapshot itself */
|
|
14
|
+
const allFields = 7;
|
|
15
|
+
const emptyRefs = Object.freeze([]);
|
|
16
|
+
var SummarySubscription = class {
|
|
17
|
+
entry;
|
|
18
|
+
options;
|
|
19
|
+
onDispose;
|
|
20
|
+
emitter = new Emitter();
|
|
21
|
+
/** Listener-less session subscriptions that carry this subscriber's timing into the session */
|
|
22
|
+
timing;
|
|
23
|
+
disposed = false;
|
|
24
|
+
constructor(entry, options, onDispose) {
|
|
25
|
+
this.entry = entry;
|
|
26
|
+
this.options = options;
|
|
27
|
+
this.onDispose = onDispose;
|
|
28
|
+
}
|
|
29
|
+
get current() {
|
|
30
|
+
return this.disposed ? void 0 : this.entry.value;
|
|
31
|
+
}
|
|
32
|
+
get onDidChange() {
|
|
33
|
+
return this.emitter.event;
|
|
34
|
+
}
|
|
35
|
+
[Symbol.dispose]() {
|
|
36
|
+
this.dispose();
|
|
37
|
+
}
|
|
38
|
+
dispose() {
|
|
39
|
+
if (this.disposed) return;
|
|
40
|
+
this.disposed = true;
|
|
41
|
+
if (this.timing != null) {
|
|
42
|
+
for (const d of this.timing) d.dispose();
|
|
43
|
+
this.timing = void 0;
|
|
44
|
+
}
|
|
45
|
+
this.emitter.dispose();
|
|
46
|
+
this.onDispose(this);
|
|
47
|
+
}
|
|
48
|
+
attach(session) {
|
|
49
|
+
if (this.disposed || this.timing != null) return;
|
|
50
|
+
this.timing = [session.subscribe({
|
|
51
|
+
delayMs: this.options?.delayMs,
|
|
52
|
+
maxWaitMs: this.options?.maxWaitMs
|
|
53
|
+
}), session.subscribeToWorkingTree({
|
|
54
|
+
delayMs: this.options?.workingTreeDelayMs,
|
|
55
|
+
maxWaitMs: this.options?.workingTreeMaxWaitMs
|
|
56
|
+
})];
|
|
57
|
+
}
|
|
58
|
+
fire(summary) {
|
|
59
|
+
this.emitter.fire(summary);
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
/**
|
|
63
|
+
* A cached, change-notifying summary of each subscribed worktree — its branch, upstream and ahead/behind, whether it
|
|
64
|
+
* has working changes, and how far it is past its base — kept current by the repository watch layer.
|
|
65
|
+
*
|
|
66
|
+
* **Lifecycle.** Entries are per repository key and ref-counted across {@link subscribe} calls. The first subscriber
|
|
67
|
+
* starts watching the worktree and reads everything; the last one to leave aborts any in-flight read, releases the
|
|
68
|
+
* watch and drops the value. Each subscriber's timing options join the watch session's cadence (the shortest wins);
|
|
69
|
+
* the service's own listening subscriptions never constrain it.
|
|
70
|
+
*
|
|
71
|
+
* **Field-scoped recompute.** The summary is read in three groups — `dirty` (working changes), `branch` (branch,
|
|
72
|
+
* HEAD's commit, upstream, ahead/behind) and `base` (ahead of base, the base's tip, and the refs it depends on) — and
|
|
73
|
+
* an event recomputes only the groups it can affect: a working-tree edit, an index or ignore-rule change reads only
|
|
74
|
+
* `dirty`; the upstream moving reads only `branch`; a ref the base depends on reads only `base`; a `gk` config change
|
|
75
|
+
* (mostly bookkeeping writes to the shared `.git/gk/config`) re-reads the branch's stored merge targets and recorded
|
|
76
|
+
* base, and reads `base` only when one of them changed; the checked-out branch moving, or a HEAD change, reads all of
|
|
77
|
+
* them. Stash, unrelated tags and branches, and the like read nothing. One run is in flight per worktree at a time;
|
|
78
|
+
* events during a run are merged into a single follow-up run.
|
|
79
|
+
*
|
|
80
|
+
* **Value-change-only notification.** {@link WorktreeSummarySubscription.onDidChange} and {@link onDidChangeSummary}
|
|
81
|
+
* fire only when some field's value differs from the previous summary — a recompute that reads the same answer is
|
|
82
|
+
* silent, so a consumer can re-render on every event without diffing. A failed recompute keeps the last values and
|
|
83
|
+
* sets `stale` (firing once); the next successful one clears it.
|
|
84
|
+
*/
|
|
85
|
+
var WorktreeSummaryService = class {
|
|
86
|
+
_onDidChangeSummary = new Emitter();
|
|
87
|
+
/** Fires when the summary of any subscribed worktree changes value */
|
|
88
|
+
get onDidChangeSummary() {
|
|
89
|
+
return this._onDidChangeSummary.event;
|
|
90
|
+
}
|
|
91
|
+
entries = /* @__PURE__ */ new Map();
|
|
92
|
+
watchService;
|
|
93
|
+
getRepository;
|
|
94
|
+
_disposed = false;
|
|
95
|
+
constructor(options) {
|
|
96
|
+
this.watchService = options.watchService;
|
|
97
|
+
this.getRepository = options.getRepository;
|
|
98
|
+
}
|
|
99
|
+
[Symbol.dispose]() {
|
|
100
|
+
this.dispose();
|
|
101
|
+
}
|
|
102
|
+
dispose() {
|
|
103
|
+
if (this._disposed) return;
|
|
104
|
+
this._disposed = true;
|
|
105
|
+
for (const entry of [...this.entries.values()]) for (const subscription of [...entry.subscribers]) subscription.dispose();
|
|
106
|
+
this.entries.clear();
|
|
107
|
+
this._onDidChangeSummary.dispose();
|
|
108
|
+
}
|
|
109
|
+
/** Subscribes to a worktree's summary, starting to watch and read it if this is its first subscriber */
|
|
110
|
+
subscribe(repoPath, options) {
|
|
111
|
+
const key = getRepositoryKey(repoPath);
|
|
112
|
+
let entry = this._disposed ? void 0 : this.entries.get(key);
|
|
113
|
+
const isNew = entry == null;
|
|
114
|
+
if (entry == null) {
|
|
115
|
+
entry = createEntry(key);
|
|
116
|
+
if (this._disposed) entry.disposed = true;
|
|
117
|
+
else this.entries.set(key, entry);
|
|
118
|
+
}
|
|
119
|
+
const target = entry;
|
|
120
|
+
const subscription = new SummarySubscription(target, options, (s) => this.release(target, s));
|
|
121
|
+
target.subscribers.add(subscription);
|
|
122
|
+
if (isNew) {
|
|
123
|
+
if (!target.disposed) {
|
|
124
|
+
target.pending = allFields;
|
|
125
|
+
target.running = this.start(target);
|
|
126
|
+
}
|
|
127
|
+
} else if (target.handle != null) subscription.attach(target.handle.session);
|
|
128
|
+
else if (target.running == null && !target.disposed) {
|
|
129
|
+
target.pending = allFields;
|
|
130
|
+
target.running = this.start(target);
|
|
131
|
+
}
|
|
132
|
+
return subscription;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Returns a worktree's summary. For a subscribed worktree, the cached value — after waiting for any run in flight
|
|
136
|
+
* or pending; it rejects with the read's error when there is still no value. Otherwise a one-off full read that
|
|
137
|
+
* is neither cached nor watched.
|
|
138
|
+
*/
|
|
139
|
+
async getSummary(repoPath, cancellation) {
|
|
140
|
+
const key = getRepositoryKey(repoPath);
|
|
141
|
+
const entry = this.entries.get(key);
|
|
142
|
+
if (entry == null) {
|
|
143
|
+
const repo = this.getRepository(key);
|
|
144
|
+
if (repo == null) throw new Error(`No repository found for '${key}'`);
|
|
145
|
+
return (await readSummary(repo, key, allFields, void 0, void 0, cancellation)).summary;
|
|
146
|
+
}
|
|
147
|
+
if (entry.running != null) await (cancellation != null ? raceWithSignal(entry.running, cancellation) : entry.running);
|
|
148
|
+
if (entry.disposed) throw new CancellationError();
|
|
149
|
+
if (entry.value != null) return entry.value;
|
|
150
|
+
throw entry.error ?? /* @__PURE__ */ new Error(`No summary available for '${key}'`);
|
|
151
|
+
}
|
|
152
|
+
getResourceUsage() {
|
|
153
|
+
return { "worktreeSummary.entries.count": this.entries.size };
|
|
154
|
+
}
|
|
155
|
+
async start(entry) {
|
|
156
|
+
let gitDir;
|
|
157
|
+
try {
|
|
158
|
+
gitDir = await this.getRepository(entry.key)?.config.getGitDir?.();
|
|
159
|
+
} catch (ex) {
|
|
160
|
+
if (entry.disposed) return;
|
|
161
|
+
entry.running = void 0;
|
|
162
|
+
if (!isCancellationError(ex)) this.fail(entry, ex);
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
if (entry.disposed) return;
|
|
166
|
+
const handle = gitDir != null ? this.watchService.watch(entry.key, gitDir) : void 0;
|
|
167
|
+
if (handle == null) {
|
|
168
|
+
entry.running = void 0;
|
|
169
|
+
this.fail(entry, /* @__PURE__ */ new Error(`Unable to watch '${entry.key}': no repository or git directory`));
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
entry.handle = handle;
|
|
173
|
+
const session = handle.session;
|
|
174
|
+
const repoSub = session.subscribe({
|
|
175
|
+
delayMs: Infinity,
|
|
176
|
+
maxWaitMs: Infinity
|
|
177
|
+
});
|
|
178
|
+
const workingTreeSub = session.subscribeToWorkingTree({
|
|
179
|
+
delayMs: Infinity,
|
|
180
|
+
maxWaitMs: Infinity
|
|
181
|
+
});
|
|
182
|
+
entry.listeners = [
|
|
183
|
+
repoSub.onDidChange((e) => this.onRepoChanged(entry, e)),
|
|
184
|
+
workingTreeSub.onDidChangeWorkingTree(() => this.queue(entry, dirtyField)),
|
|
185
|
+
repoSub,
|
|
186
|
+
workingTreeSub
|
|
187
|
+
];
|
|
188
|
+
for (const subscription of entry.subscribers) subscription.attach(session);
|
|
189
|
+
await this.drain(entry);
|
|
190
|
+
}
|
|
191
|
+
onRepoChanged(entry, e) {
|
|
192
|
+
const value = entry.value;
|
|
193
|
+
if (value == null || e.changed("unknown", "head")) {
|
|
194
|
+
this.queue(entry, allFields);
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
let fields = 0;
|
|
198
|
+
if (e.changed("index", "ignores")) fields |= dirtyField;
|
|
199
|
+
if (e.changed("config")) fields |= 6;
|
|
200
|
+
if (e.changed("gkConfig")) fields |= baseKeysField;
|
|
201
|
+
if (value.branch != null && e.mayHaveChangedRef(`refs/heads/${value.branch}`)) fields |= allFields;
|
|
202
|
+
if (value.upstream != null && (e.mayHaveChangedRef(`refs/remotes/${value.upstream.name}`) || e.mayHaveChangedRef(`refs/heads/${value.upstream.name}`))) fields |= branchField;
|
|
203
|
+
if ((fields & baseField) === 0) {
|
|
204
|
+
for (const ref of value.baseDependsOn) if (e.mayHaveChangedRef(ref)) {
|
|
205
|
+
fields |= baseField;
|
|
206
|
+
break;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
this.queue(entry, fields);
|
|
210
|
+
}
|
|
211
|
+
queue(entry, fields) {
|
|
212
|
+
if (entry.disposed || fields === 0) return;
|
|
213
|
+
entry.pending |= entry.value == null || entry.value.stale ? allFields : fields;
|
|
214
|
+
entry.running ??= this.drain(entry);
|
|
215
|
+
}
|
|
216
|
+
/** Runs until nothing is pending. Never rejects — every failure becomes entry state. */
|
|
217
|
+
async drain(entry) {
|
|
218
|
+
while (entry.pending !== 0 && !entry.disposed) {
|
|
219
|
+
const fields = entry.pending;
|
|
220
|
+
entry.pending = 0;
|
|
221
|
+
await this.run(entry, fields);
|
|
222
|
+
}
|
|
223
|
+
entry.running = void 0;
|
|
224
|
+
}
|
|
225
|
+
async run(entry, fields) {
|
|
226
|
+
const previous = entry.value;
|
|
227
|
+
let result;
|
|
228
|
+
try {
|
|
229
|
+
const repo = this.getRepository(entry.key);
|
|
230
|
+
if (repo == null) throw new Error(`No repository found for '${entry.key}'`);
|
|
231
|
+
result = await readSummary(repo, entry.key, fields, previous, entry.baseKeys, entry.abort.signal);
|
|
232
|
+
} catch (ex) {
|
|
233
|
+
if (entry.disposed || isCancellationError(ex)) return;
|
|
234
|
+
this.fail(entry, ex);
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
if (entry.disposed) return;
|
|
238
|
+
if (result.summary.stale && (previous == null || !previous.stale)) _logger.warn(`WorktreeSummaryService: reading the base of '${entry.key}' failed: ${String(result.baseError)}`);
|
|
239
|
+
entry.error = void 0;
|
|
240
|
+
entry.baseKeys = result.baseKeys;
|
|
241
|
+
if (previous != null && summariesEqual(previous, result.summary)) return;
|
|
242
|
+
entry.value = result.summary;
|
|
243
|
+
this.fireChange(entry, result.summary);
|
|
244
|
+
}
|
|
245
|
+
fail(entry, ex) {
|
|
246
|
+
const previous = entry.value;
|
|
247
|
+
if (previous == null) {
|
|
248
|
+
if (entry.error == null) _logger.warn(`WorktreeSummaryService: reading '${entry.key}' failed: ${String(ex)}`);
|
|
249
|
+
entry.error = ex instanceof Error ? ex : new Error(String(ex));
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
if (previous.stale) return;
|
|
253
|
+
_logger.warn(`WorktreeSummaryService: refreshing '${entry.key}' failed: ${String(ex)}`);
|
|
254
|
+
const summary = {
|
|
255
|
+
...previous,
|
|
256
|
+
stale: true
|
|
257
|
+
};
|
|
258
|
+
entry.value = summary;
|
|
259
|
+
this.fireChange(entry, summary);
|
|
260
|
+
}
|
|
261
|
+
fireChange(entry, summary) {
|
|
262
|
+
for (const subscription of entry.subscribers) subscription.fire(summary);
|
|
263
|
+
this._onDidChangeSummary.fire(summary);
|
|
264
|
+
}
|
|
265
|
+
release(entry, subscription) {
|
|
266
|
+
entry.subscribers.delete(subscription);
|
|
267
|
+
if (entry.subscribers.size !== 0) return;
|
|
268
|
+
entry.disposed = true;
|
|
269
|
+
entry.abort.abort();
|
|
270
|
+
if (entry.listeners != null) {
|
|
271
|
+
for (const d of entry.listeners) d.dispose();
|
|
272
|
+
entry.listeners = void 0;
|
|
273
|
+
}
|
|
274
|
+
entry.handle?.dispose();
|
|
275
|
+
entry.handle = void 0;
|
|
276
|
+
entry.value = void 0;
|
|
277
|
+
entry.baseKeys = void 0;
|
|
278
|
+
entry.pending = 0;
|
|
279
|
+
entry.running = void 0;
|
|
280
|
+
if (this.entries.get(entry.key) === entry) this.entries.delete(entry.key);
|
|
281
|
+
}
|
|
282
|
+
};
|
|
283
|
+
function createEntry(key) {
|
|
284
|
+
return {
|
|
285
|
+
key,
|
|
286
|
+
abort: new AbortController(),
|
|
287
|
+
subscribers: /* @__PURE__ */ new Set(),
|
|
288
|
+
handle: void 0,
|
|
289
|
+
listeners: void 0,
|
|
290
|
+
value: void 0,
|
|
291
|
+
baseKeys: void 0,
|
|
292
|
+
error: void 0,
|
|
293
|
+
pending: 0,
|
|
294
|
+
running: void 0,
|
|
295
|
+
disposed: false
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
299
|
+
* Reads the due groups and fills the rest from `previous` (every group when there is none). The branch read comes
|
|
300
|
+
* before the base read, which needs its name; the dirty read runs alongside both. A branch read that finds a
|
|
301
|
+
* different branch (or detached state) also reads the base and dirty groups, and stored base values that differ
|
|
302
|
+
* from `previousBaseKeys` read the base group. A failed base read doesn't fail the rest: the summary comes back stale,
|
|
303
|
+
* keeping the previous base and `baseDependsOn` only while the branch is the same.
|
|
304
|
+
*/
|
|
305
|
+
async function readSummary(repo, key, fields, previous, previousBaseKeys, signal) {
|
|
306
|
+
if (previous == null) fields = allFields;
|
|
307
|
+
let dirtyRead = fields & dirtyField ? readDirty(repo, signal) : void 0;
|
|
308
|
+
let branch = previous?.branch;
|
|
309
|
+
let detached = previous?.detached ?? false;
|
|
310
|
+
let sha = previous?.sha;
|
|
311
|
+
let upstream = previous?.upstream;
|
|
312
|
+
let ahead = previous?.ahead ?? 0;
|
|
313
|
+
let behind = previous?.behind ?? 0;
|
|
314
|
+
if (fields & branchField) {
|
|
315
|
+
const current = await repo.branches.getBranch(void 0, void 0, signal);
|
|
316
|
+
detached = current?.detached ?? false;
|
|
317
|
+
branch = detached ? void 0 : current?.name || void 0;
|
|
318
|
+
sha = current?.sha;
|
|
319
|
+
upstream = current?.upstream != null ? await toSummaryUpstream(repo, current.upstream, signal) : void 0;
|
|
320
|
+
ahead = current?.upstream?.state.ahead ?? 0;
|
|
321
|
+
behind = current?.upstream?.state.behind ?? 0;
|
|
322
|
+
if (previous != null && (branch !== previous.branch || detached !== previous.detached)) {
|
|
323
|
+
fields |= baseField;
|
|
324
|
+
dirtyRead ??= readDirty(repo, signal);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
let base = previous?.base;
|
|
328
|
+
let dependsOn = previous?.baseDependsOn ?? emptyRefs;
|
|
329
|
+
let baseKeys = previousBaseKeys;
|
|
330
|
+
let baseKeysRead = false;
|
|
331
|
+
let baseFailed = false;
|
|
332
|
+
let baseError;
|
|
333
|
+
const baseBranch = !detached && repo.branches.getAheadOfBase != null ? branch : void 0;
|
|
334
|
+
if (fields & baseKeysField && !(fields & baseField) && baseBranch != null) {
|
|
335
|
+
baseKeys = await readBaseKeys(repo, baseBranch);
|
|
336
|
+
baseKeysRead = true;
|
|
337
|
+
if (baseKeys == null || previousBaseKeys == null || !baseKeysEqual(baseKeys, previousBaseKeys)) fields |= baseField;
|
|
338
|
+
}
|
|
339
|
+
if (fields & baseField) {
|
|
340
|
+
if (baseBranch == null) {
|
|
341
|
+
base = void 0;
|
|
342
|
+
dependsOn = emptyRefs;
|
|
343
|
+
baseKeys = void 0;
|
|
344
|
+
} else {
|
|
345
|
+
if (!baseKeysRead) baseKeys = await readBaseKeys(repo, baseBranch);
|
|
346
|
+
try {
|
|
347
|
+
const aheadOfBase = await repo.branches.getAheadOfBase?.(baseBranch, signal);
|
|
348
|
+
base = aheadOfBase?.base;
|
|
349
|
+
dependsOn = aheadOfBase?.dependsOn ?? emptyRefs;
|
|
350
|
+
} catch (ex) {
|
|
351
|
+
if (isCancellationError(ex)) throw ex;
|
|
352
|
+
baseFailed = true;
|
|
353
|
+
baseError = ex;
|
|
354
|
+
const sameBranch = previous != null && branch === previous.branch && detached === previous.detached;
|
|
355
|
+
base = sameBranch ? previous.base : void 0;
|
|
356
|
+
dependsOn = sameBranch ? previous.baseDependsOn : emptyRefs;
|
|
357
|
+
baseKeys = void 0;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
const dirty = dirtyRead != null ? await dirtyRead : previous?.dirty ?? false;
|
|
362
|
+
return {
|
|
363
|
+
summary: {
|
|
364
|
+
repoPath: key,
|
|
365
|
+
branch,
|
|
366
|
+
detached,
|
|
367
|
+
sha,
|
|
368
|
+
upstream,
|
|
369
|
+
ahead,
|
|
370
|
+
behind,
|
|
371
|
+
dirty,
|
|
372
|
+
base,
|
|
373
|
+
baseDependsOn: dependsOn,
|
|
374
|
+
stale: baseFailed
|
|
375
|
+
},
|
|
376
|
+
baseKeys,
|
|
377
|
+
baseError
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
/**
|
|
381
|
+
* Reads {@link BaseKeys} for `branch`, normalized the way `getAheadOfBase` reads them. `undefined` when any read
|
|
382
|
+
* failed — an unknown snapshot, so the next `gk` config change reads the base rather than trusting it.
|
|
383
|
+
*/
|
|
384
|
+
async function readBaseKeys(repo, branch) {
|
|
385
|
+
try {
|
|
386
|
+
const [user, detected, recorded] = await Promise.all([
|
|
387
|
+
repo.branches.getStoredUserMergeTargetBranchName?.(branch),
|
|
388
|
+
repo.branches.getStoredDetectedMergeTargetBranchName?.(branch),
|
|
389
|
+
repo.config.getGkConfig?.(`branch.${branch}.gk-merge-base`)
|
|
390
|
+
]);
|
|
391
|
+
return [
|
|
392
|
+
user?.trim() || void 0,
|
|
393
|
+
detected?.trim() || void 0,
|
|
394
|
+
recorded?.trim() || void 0
|
|
395
|
+
];
|
|
396
|
+
} catch (ex) {
|
|
397
|
+
if (isCancellationError(ex)) throw ex;
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
function baseKeysEqual(a, b) {
|
|
402
|
+
return a[0] === b[0] && a[1] === b[1] && a[2] === b[2];
|
|
403
|
+
}
|
|
404
|
+
function readDirty(repo, signal) {
|
|
405
|
+
const read = repo.status.hasWorkingChanges({ untracked: true }, signal);
|
|
406
|
+
read.catch(noop);
|
|
407
|
+
return read;
|
|
408
|
+
}
|
|
409
|
+
function noop() {}
|
|
410
|
+
/** Splits the upstream at the longest remote name it starts with; no split when none matches or nothing follows it */
|
|
411
|
+
async function toSummaryUpstream(repo, upstream, signal) {
|
|
412
|
+
const name = upstream.name;
|
|
413
|
+
let remote;
|
|
414
|
+
for (const r of await repo.remotes.getRemotes(void 0, signal)) if (r.name && name.startsWith(`${r.name}/`) && (remote == null || r.name.length > remote.length)) remote = r.name;
|
|
415
|
+
const branch = remote != null ? name.substring(remote.length + 1) : void 0;
|
|
416
|
+
return {
|
|
417
|
+
name,
|
|
418
|
+
remote: branch ? remote : void 0,
|
|
419
|
+
branch: branch || void 0,
|
|
420
|
+
missing: upstream.missing
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
function summariesEqual(a, b) {
|
|
424
|
+
return a.repoPath === b.repoPath && a.branch === b.branch && a.detached === b.detached && a.sha === b.sha && a.ahead === b.ahead && a.behind === b.behind && a.dirty === b.dirty && a.stale === b.stale && areEqual(a.baseDependsOn, b.baseDependsOn) && (a.upstream === b.upstream || a.upstream != null && b.upstream != null && a.upstream.name === b.upstream.name && a.upstream.remote === b.upstream.remote && a.upstream.branch === b.upstream.branch && a.upstream.missing === b.upstream.missing) && (a.base === b.base || a.base != null && b.base != null && a.base.ref === b.base.ref && a.base.ahead === b.base.ahead && a.base.sha === b.base.sha);
|
|
425
|
+
}
|
|
426
|
+
//#endregion
|
|
427
|
+
export { WorktreeSummaryService };
|
|
428
|
+
|
|
429
|
+
//# sourceMappingURL=worktreeSummary.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"worktreeSummary.js","names":[],"sources":["../../../../git/src/watching/worktreeSummary.ts"],"sourcesContent":["import { areEqual } from '@gitlens/utils/array.js';\nimport { CancellationError, isCancellationError, raceWithSignal } from '@gitlens/utils/cancellation.js';\nimport type { UnifiedDisposable } from '@gitlens/utils/disposable.js';\nimport type { Event } from '@gitlens/utils/event.js';\nimport { Emitter } from '@gitlens/utils/event.js';\nimport { Logger } from '@gitlens/utils/logger.js';\nimport type { ResourceUsage } from '@gitlens/utils/resourceUsage.js';\nimport { getRepositoryKey } from '@gitlens/utils/uri.js';\nimport type { GitTrackingUpstream } from '../models/branch.js';\nimport type { GitDir } from '../models/repository.js';\nimport type { RepositoryService } from '../repositoryService.js';\nimport type { WatcherRepoChangeEvent } from './changeEvent.js';\nimport type { RepositoryWatchService, WatchHandle } from './watchService.js';\nimport type { RepositoryWatchSession } from './watchSession.js';\n\nexport interface WorktreeSummaryUpstream {\n\t/** The upstream's short name (`origin/main`) */\n\treadonly name: string;\n\t/**\n\t * The remote it tracks: the longest of the repository's remote names that {@link name} starts with, followed by\n\t * `/` (a remote name can hold a slash). `undefined`, with {@link branch}, when it names no known remote (a branch\n\t * tracking another local branch, or a remote since removed)\n\t */\n\treadonly remote: string | undefined;\n\t/** The branch on {@link remote}; `undefined` whenever {@link remote} is */\n\treadonly branch: string | undefined;\n\treadonly missing: boolean;\n}\n\nexport interface WorktreeSummaryBase {\n\t/** The ref the count was taken against (see `AheadOfBase.base`) */\n\treadonly ref: string;\n\treadonly ahead: number;\n\t/** The commit {@link ref} pointed at when the count was read; `undefined` when it couldn't be resolved */\n\treadonly sha: string | undefined;\n}\n\nexport interface WorktreeSummary {\n\t/** The repository path in {@link getRepositoryKey} form */\n\treadonly repoPath: string;\n\t/** The checked-out branch; `undefined` when detached (or unborn with no name) */\n\treadonly branch: string | undefined;\n\treadonly detached: boolean;\n\t/** HEAD's commit; `undefined` when HEAD has no commit yet */\n\treadonly sha: string | undefined;\n\treadonly upstream: WorktreeSummaryUpstream | undefined;\n\t/** Commits ahead of the upstream; 0 without one */\n\treadonly ahead: number;\n\t/** Commits behind the upstream; 0 without one */\n\treadonly behind: number;\n\t/** Whether there is any staged, unstaged or untracked (non-ignored) change */\n\treadonly dirty: boolean;\n\t/** How far the branch is past its base; `undefined` when detached, unsupported, or there is no base */\n\treadonly base: WorktreeSummaryBase | undefined;\n\t/**\n\t * Full ref names whose movement can change {@link base}, as `getAheadOfBase` reports them (see\n\t * `AheadOfBase.dependsOn`) — given with or without a base, so a watcher also learns when one appears. Empty when\n\t * the base isn't read (detached, or a provider without `getAheadOfBase`), and when the base read failed with no\n\t * earlier list for this branch (the first read, or the one after a checkout).\n\t */\n\treadonly baseDependsOn: readonly string[];\n\t/**\n\t * The latest recompute failed (worktree gone, lock contention); the other fields hold the last values read. Also set\n\t * when only the base read failed: the other fields are fresh, and `base` and `baseDependsOn` are their last values,\n\t * or `undefined` and empty if the branch changed.\n\t */\n\treadonly stale: boolean;\n}\n\nexport interface WorktreeSummarySubscribeOptions {\n\t/** Debounce for repo (`.git`) changes this subscriber asks of the watch session; the shortest across subscribers wins */\n\treadonly delayMs?: number;\n\t/** Cap on how long continuous repo activity can defer a recompute; `Infinity` means no cap */\n\treadonly maxWaitMs?: number;\n\t/** Debounce for working-tree changes this subscriber asks of the watch session */\n\treadonly workingTreeDelayMs?: number;\n\t/** Cap on how long continuous working-tree activity can defer a recompute; `Infinity` means no cap */\n\treadonly workingTreeMaxWaitMs?: number;\n}\n\nexport interface WorktreeSummarySubscription extends UnifiedDisposable {\n\t/** The latest summary; `undefined` until the first read succeeds, or after disposal */\n\treadonly current: WorktreeSummary | undefined;\n\t/** Fires when the summary's value changes */\n\treadonly onDidChange: Event<WorktreeSummary>;\n}\n\nexport interface WorktreeSummaryServiceOptions {\n\treadonly watchService: RepositoryWatchService;\n\treadonly getRepository: (repoPath: string) => RepositoryService | undefined;\n}\n\n/** Read groups, as bit flags so a pending set is a number rather than an allocation per event */\nconst dirtyField = 1;\nconst branchField = 2;\nconst baseField = 4;\n/** Re-reads only the stored values the base read consults, and the base itself only when they changed */\nconst baseKeysField = 8;\n/** Every group a full read covers — a base read refreshes the {@link BaseKeys} snapshot itself */\nconst allFields = dirtyField | branchField | baseField;\n\nconst emptyRefs: readonly string[] = Object.freeze([]);\n\n/** The current branch's stored merge targets (user, detected) and recorded base, as `getAheadOfBase` consults them */\ntype BaseKeys = readonly [string | undefined, string | undefined, string | undefined];\n\ninterface ReadResult {\n\treadonly summary: WorktreeSummary;\n\treadonly baseKeys: BaseKeys | undefined;\n\t/** Why the base read failed when the rest of the summary was read; the summary is then stale */\n\treadonly baseError: unknown;\n}\n\ninterface SummaryEntry {\n\treadonly key: string;\n\t/** Aborted when the last subscriber leaves, cancelling any in-flight read */\n\treadonly abort: AbortController;\n\treadonly subscribers: Set<SummarySubscription>;\n\thandle: WatchHandle | undefined;\n\t/** The timing-neutral listening subscriptions and their event listeners */\n\tlisteners: UnifiedDisposable[] | undefined;\n\tvalue: WorktreeSummary | undefined;\n\t/** The stored base values as of the last base read; a `gk` config change re-reads the base only when they differ */\n\tbaseKeys: BaseKeys | undefined;\n\t/** Why the latest read failed while there was no value to keep */\n\terror: Error | undefined;\n\t/** Read groups due on the next run */\n\tpending: number;\n\t/** The active run (or the start-up, which ends in one); cleared in the same turn the loop sees nothing pending */\n\trunning: Promise<void> | undefined;\n\tdisposed: boolean;\n}\n\nclass SummarySubscription implements WorktreeSummarySubscription {\n\tprivate readonly emitter = new Emitter<WorktreeSummary>();\n\t/** Listener-less session subscriptions that carry this subscriber's timing into the session */\n\tprivate timing: UnifiedDisposable[] | undefined;\n\tprivate disposed = false;\n\n\tconstructor(\n\t\tprivate readonly entry: SummaryEntry,\n\t\tprivate readonly options: WorktreeSummarySubscribeOptions | undefined,\n\t\tprivate readonly onDispose: (subscription: SummarySubscription) => void,\n\t) {}\n\n\tget current(): WorktreeSummary | undefined {\n\t\treturn this.disposed ? undefined : this.entry.value;\n\t}\n\n\tget onDidChange(): Event<WorktreeSummary> {\n\t\treturn this.emitter.event;\n\t}\n\n\t[Symbol.dispose](): void {\n\t\tthis.dispose();\n\t}\n\n\tdispose(): void {\n\t\tif (this.disposed) return;\n\n\t\tthis.disposed = true;\n\t\tif (this.timing != null) {\n\t\t\tfor (const d of this.timing) {\n\t\t\t\td.dispose();\n\t\t\t}\n\t\t\tthis.timing = undefined;\n\t\t}\n\t\tthis.emitter.dispose();\n\t\tthis.onDispose(this);\n\t}\n\n\tattach(session: RepositoryWatchSession): void {\n\t\tif (this.disposed || this.timing != null) return;\n\n\t\tthis.timing = [\n\t\t\tsession.subscribe({ delayMs: this.options?.delayMs, maxWaitMs: this.options?.maxWaitMs }),\n\t\t\tsession.subscribeToWorkingTree({\n\t\t\t\tdelayMs: this.options?.workingTreeDelayMs,\n\t\t\t\tmaxWaitMs: this.options?.workingTreeMaxWaitMs,\n\t\t\t}),\n\t\t];\n\t}\n\n\tfire(summary: WorktreeSummary): void {\n\t\tthis.emitter.fire(summary);\n\t}\n}\n\n/**\n * A cached, change-notifying summary of each subscribed worktree — its branch, upstream and ahead/behind, whether it\n * has working changes, and how far it is past its base — kept current by the repository watch layer.\n *\n * **Lifecycle.** Entries are per repository key and ref-counted across {@link subscribe} calls. The first subscriber\n * starts watching the worktree and reads everything; the last one to leave aborts any in-flight read, releases the\n * watch and drops the value. Each subscriber's timing options join the watch session's cadence (the shortest wins);\n * the service's own listening subscriptions never constrain it.\n *\n * **Field-scoped recompute.** The summary is read in three groups — `dirty` (working changes), `branch` (branch,\n * HEAD's commit, upstream, ahead/behind) and `base` (ahead of base, the base's tip, and the refs it depends on) — and\n * an event recomputes only the groups it can affect: a working-tree edit, an index or ignore-rule change reads only\n * `dirty`; the upstream moving reads only `branch`; a ref the base depends on reads only `base`; a `gk` config change\n * (mostly bookkeeping writes to the shared `.git/gk/config`) re-reads the branch's stored merge targets and recorded\n * base, and reads `base` only when one of them changed; the checked-out branch moving, or a HEAD change, reads all of\n * them. Stash, unrelated tags and branches, and the like read nothing. One run is in flight per worktree at a time;\n * events during a run are merged into a single follow-up run.\n *\n * **Value-change-only notification.** {@link WorktreeSummarySubscription.onDidChange} and {@link onDidChangeSummary}\n * fire only when some field's value differs from the previous summary — a recompute that reads the same answer is\n * silent, so a consumer can re-render on every event without diffing. A failed recompute keeps the last values and\n * sets `stale` (firing once); the next successful one clears it.\n */\nexport class WorktreeSummaryService implements UnifiedDisposable {\n\tprivate readonly _onDidChangeSummary = new Emitter<WorktreeSummary>();\n\t/** Fires when the summary of any subscribed worktree changes value */\n\tget onDidChangeSummary(): Event<WorktreeSummary> {\n\t\treturn this._onDidChangeSummary.event;\n\t}\n\n\tprivate readonly entries = new Map<string, SummaryEntry>();\n\tprivate readonly watchService: RepositoryWatchService;\n\tprivate readonly getRepository: (repoPath: string) => RepositoryService | undefined;\n\tprivate _disposed = false;\n\n\tconstructor(options: WorktreeSummaryServiceOptions) {\n\t\tthis.watchService = options.watchService;\n\t\tthis.getRepository = options.getRepository;\n\t}\n\n\t[Symbol.dispose](): void {\n\t\tthis.dispose();\n\t}\n\n\tdispose(): void {\n\t\tif (this._disposed) return;\n\n\t\tthis._disposed = true;\n\t\tfor (const entry of [...this.entries.values()]) {\n\t\t\tfor (const subscription of [...entry.subscribers]) {\n\t\t\t\tsubscription.dispose();\n\t\t\t}\n\t\t}\n\t\tthis.entries.clear();\n\t\tthis._onDidChangeSummary.dispose();\n\t}\n\n\t/** Subscribes to a worktree's summary, starting to watch and read it if this is its first subscriber */\n\tsubscribe(repoPath: string, options?: WorktreeSummarySubscribeOptions): WorktreeSummarySubscription {\n\t\tconst key = getRepositoryKey(repoPath);\n\n\t\tlet entry = this._disposed ? undefined : this.entries.get(key);\n\t\tconst isNew = entry == null;\n\t\tif (entry == null) {\n\t\t\tentry = createEntry(key);\n\t\t\tif (this._disposed) {\n\t\t\t\tentry.disposed = true;\n\t\t\t} else {\n\t\t\t\tthis.entries.set(key, entry);\n\t\t\t}\n\t\t}\n\n\t\tconst target = entry;\n\t\tconst subscription = new SummarySubscription(target, options, s => this.release(target, s));\n\t\ttarget.subscribers.add(subscription);\n\n\t\tif (isNew) {\n\t\t\tif (!target.disposed) {\n\t\t\t\ttarget.pending = allFields;\n\t\t\t\ttarget.running = this.start(target);\n\t\t\t}\n\t\t} else if (target.handle != null) {\n\t\t\tsubscription.attach(target.handle.session);\n\t\t} else if (target.running == null && !target.disposed) {\n\t\t\t// The start failed (no repository or git directory yet); a new subscriber retries it\n\t\t\ttarget.pending = allFields;\n\t\t\ttarget.running = this.start(target);\n\t\t}\n\n\t\treturn subscription;\n\t}\n\n\t/**\n\t * Returns a worktree's summary. For a subscribed worktree, the cached value — after waiting for any run in flight\n\t * or pending; it rejects with the read's error when there is still no value. Otherwise a one-off full read that\n\t * is neither cached nor watched.\n\t */\n\tasync getSummary(repoPath: string, cancellation?: AbortSignal): Promise<WorktreeSummary> {\n\t\tconst key = getRepositoryKey(repoPath);\n\t\tconst entry = this.entries.get(key);\n\t\tif (entry == null) {\n\t\t\tconst repo = this.getRepository(key);\n\t\t\tif (repo == null) throw new Error(`No repository found for '${key}'`);\n\n\t\t\tconst result = await readSummary(repo, key, allFields, undefined, undefined, cancellation);\n\t\t\treturn result.summary;\n\t\t}\n\n\t\tif (entry.running != null) {\n\t\t\tawait (cancellation != null ? raceWithSignal(entry.running, cancellation) : entry.running);\n\t\t}\n\t\tif (entry.disposed) throw new CancellationError();\n\t\tif (entry.value != null) return entry.value;\n\n\t\tthrow entry.error ?? new Error(`No summary available for '${key}'`);\n\t}\n\n\tgetResourceUsage(): ResourceUsage {\n\t\treturn { 'worktreeSummary.entries.count': this.entries.size };\n\t}\n\n\tprivate async start(entry: SummaryEntry): Promise<void> {\n\t\tlet gitDir: GitDir | undefined;\n\t\ttry {\n\t\t\tgitDir = await this.getRepository(entry.key)?.config.getGitDir?.();\n\t\t} catch (ex) {\n\t\t\tif (entry.disposed) return;\n\n\t\t\tentry.running = undefined;\n\t\t\tif (!isCancellationError(ex)) {\n\t\t\t\tthis.fail(entry, ex);\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\t\tif (entry.disposed) return;\n\n\t\tconst handle = gitDir != null ? this.watchService.watch(entry.key, gitDir) : undefined;\n\t\tif (handle == null) {\n\t\t\tentry.running = undefined;\n\t\t\tthis.fail(entry, new Error(`Unable to watch '${entry.key}': no repository or git directory`));\n\t\t\treturn;\n\t\t}\n\n\t\tentry.handle = handle;\n\t\tconst session = handle.session;\n\t\t// Timing-neutral: the cadence comes from the subscribers' own session subscriptions (see `attach`)\n\t\tconst repoSub = session.subscribe({ delayMs: Infinity, maxWaitMs: Infinity });\n\t\tconst workingTreeSub = session.subscribeToWorkingTree({ delayMs: Infinity, maxWaitMs: Infinity });\n\t\tentry.listeners = [\n\t\t\trepoSub.onDidChange(e => this.onRepoChanged(entry, e)),\n\t\t\tworkingTreeSub.onDidChangeWorkingTree(() => this.queue(entry, dirtyField)),\n\t\t\trepoSub,\n\t\t\tworkingTreeSub,\n\t\t];\n\t\tfor (const subscription of entry.subscribers) {\n\t\t\tsubscription.attach(session);\n\t\t}\n\n\t\tawait this.drain(entry);\n\t}\n\n\tprivate onRepoChanged(entry: SummaryEntry, e: WatcherRepoChangeEvent): void {\n\t\tconst value = entry.value;\n\t\tif (value == null || e.changed('unknown', 'head')) {\n\t\t\tthis.queue(entry, allFields);\n\t\t\treturn;\n\t\t}\n\n\t\tlet fields = 0;\n\t\tif (e.changed('index', 'ignores')) {\n\t\t\tfields |= dirtyField;\n\t\t}\n\t\tif (e.changed('config')) {\n\t\t\tfields |= branchField | baseField;\n\t\t}\n\t\tif (e.changed('gkConfig')) {\n\t\t\tfields |= baseKeysField;\n\t\t}\n\t\tif (value.branch != null && e.mayHaveChangedRef(`refs/heads/${value.branch}`)) {\n\t\t\tfields |= allFields;\n\t\t}\n\t\t// The upstream's short name doesn't say whether it's remote or local (`--track main`), so test both forms\n\t\tif (\n\t\t\tvalue.upstream != null &&\n\t\t\t(e.mayHaveChangedRef(`refs/remotes/${value.upstream.name}`) ||\n\t\t\t\te.mayHaveChangedRef(`refs/heads/${value.upstream.name}`))\n\t\t) {\n\t\t\tfields |= branchField;\n\t\t}\n\t\tif ((fields & baseField) === 0) {\n\t\t\tfor (const ref of value.baseDependsOn) {\n\t\t\t\tif (e.mayHaveChangedRef(ref)) {\n\t\t\t\t\tfields |= baseField;\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tthis.queue(entry, fields);\n\t}\n\n\tprivate queue(entry: SummaryEntry, fields: number): void {\n\t\tif (entry.disposed || fields === 0) return;\n\n\t\t// Without a value a partial read has nothing to merge into, and a stale value's fields can't be trusted: a\n\t\t// partial read would clear `stale` while vouching for groups it never re-read\n\t\tentry.pending |= entry.value == null || entry.value.stale ? allFields : fields;\n\t\tentry.running ??= this.drain(entry);\n\t}\n\n\t/** Runs until nothing is pending. Never rejects — every failure becomes entry state. */\n\tprivate async drain(entry: SummaryEntry): Promise<void> {\n\t\twhile (entry.pending !== 0 && !entry.disposed) {\n\t\t\tconst fields = entry.pending;\n\t\t\tentry.pending = 0;\n\t\t\tawait this.run(entry, fields);\n\t\t}\n\t\t// Same turn as the loop's last check, so an event can't queue fields that no run will pick up\n\t\tentry.running = undefined;\n\t}\n\n\tprivate async run(entry: SummaryEntry, fields: number): Promise<void> {\n\t\tconst previous = entry.value;\n\n\t\tlet result: ReadResult;\n\t\ttry {\n\t\t\tconst repo = this.getRepository(entry.key);\n\t\t\tif (repo == null) throw new Error(`No repository found for '${entry.key}'`);\n\n\t\t\tresult = await readSummary(repo, entry.key, fields, previous, entry.baseKeys, entry.abort.signal);\n\t\t} catch (ex) {\n\t\t\tif (entry.disposed || isCancellationError(ex)) return;\n\n\t\t\tthis.fail(entry, ex);\n\t\t\treturn;\n\t\t}\n\t\tif (entry.disposed) return;\n\n\t\tif (result.summary.stale && (previous == null || !previous.stale)) {\n\t\t\tLogger.warn(\n\t\t\t\t`WorktreeSummaryService: reading the base of '${entry.key}' failed: ${String(result.baseError)}`,\n\t\t\t);\n\t\t}\n\n\t\tentry.error = undefined;\n\t\tentry.baseKeys = result.baseKeys;\n\t\tif (previous != null && summariesEqual(previous, result.summary)) return;\n\n\t\tentry.value = result.summary;\n\t\tthis.fireChange(entry, result.summary);\n\t}\n\n\tprivate fail(entry: SummaryEntry, ex: unknown): void {\n\t\tconst previous = entry.value;\n\t\tif (previous == null) {\n\t\t\tif (entry.error == null) {\n\t\t\t\tLogger.warn(`WorktreeSummaryService: reading '${entry.key}' failed: ${String(ex)}`);\n\t\t\t}\n\t\t\tentry.error = ex instanceof Error ? ex : new Error(String(ex));\n\t\t\treturn;\n\t\t}\n\t\tif (previous.stale) return;\n\n\t\tLogger.warn(`WorktreeSummaryService: refreshing '${entry.key}' failed: ${String(ex)}`);\n\t\tconst summary: WorktreeSummary = { ...previous, stale: true };\n\t\tentry.value = summary;\n\t\tthis.fireChange(entry, summary);\n\t}\n\n\tprivate fireChange(entry: SummaryEntry, summary: WorktreeSummary): void {\n\t\tfor (const subscription of entry.subscribers) {\n\t\t\tsubscription.fire(summary);\n\t\t}\n\t\tthis._onDidChangeSummary.fire(summary);\n\t}\n\n\tprivate release(entry: SummaryEntry, subscription: SummarySubscription): void {\n\t\tentry.subscribers.delete(subscription);\n\t\tif (entry.subscribers.size !== 0) return;\n\n\t\tentry.disposed = true;\n\t\tentry.abort.abort();\n\t\tif (entry.listeners != null) {\n\t\t\tfor (const d of entry.listeners) {\n\t\t\t\td.dispose();\n\t\t\t}\n\t\t\tentry.listeners = undefined;\n\t\t}\n\t\tentry.handle?.dispose();\n\t\tentry.handle = undefined;\n\t\tentry.value = undefined;\n\t\tentry.baseKeys = undefined;\n\t\tentry.pending = 0;\n\t\tentry.running = undefined;\n\n\t\tif (this.entries.get(entry.key) === entry) {\n\t\t\tthis.entries.delete(entry.key);\n\t\t}\n\t}\n}\n\nfunction createEntry(key: string): SummaryEntry {\n\treturn {\n\t\tkey: key,\n\t\tabort: new AbortController(),\n\t\tsubscribers: new Set(),\n\t\thandle: undefined,\n\t\tlisteners: undefined,\n\t\tvalue: undefined,\n\t\tbaseKeys: undefined,\n\t\terror: undefined,\n\t\tpending: 0,\n\t\trunning: undefined,\n\t\tdisposed: false,\n\t};\n}\n\n/**\n * Reads the due groups and fills the rest from `previous` (every group when there is none). The branch read comes\n * before the base read, which needs its name; the dirty read runs alongside both. A branch read that finds a\n * different branch (or detached state) also reads the base and dirty groups, and stored base values that differ\n * from `previousBaseKeys` read the base group. A failed base read doesn't fail the rest: the summary comes back stale,\n * keeping the previous base and `baseDependsOn` only while the branch is the same.\n */\nasync function readSummary(\n\trepo: RepositoryService,\n\tkey: string,\n\tfields: number,\n\tprevious: WorktreeSummary | undefined,\n\tpreviousBaseKeys: BaseKeys | undefined,\n\tsignal: AbortSignal | undefined,\n): Promise<ReadResult> {\n\tif (previous == null) {\n\t\tfields = allFields;\n\t}\n\n\tlet dirtyRead = fields & dirtyField ? readDirty(repo, signal) : undefined;\n\n\tlet branch = previous?.branch;\n\tlet detached = previous?.detached ?? false;\n\tlet sha = previous?.sha;\n\tlet upstream = previous?.upstream;\n\tlet ahead = previous?.ahead ?? 0;\n\tlet behind = previous?.behind ?? 0;\n\tif (fields & branchField) {\n\t\tconst current = await repo.branches.getBranch(undefined, undefined, signal);\n\t\tdetached = current?.detached ?? false;\n\t\tbranch = detached ? undefined : current?.name || undefined;\n\t\tsha = current?.sha;\n\t\tupstream = current?.upstream != null ? await toSummaryUpstream(repo, current.upstream, signal) : undefined;\n\t\tahead = current?.upstream?.state.ahead ?? 0;\n\t\tbehind = current?.upstream?.state.behind ?? 0;\n\n\t\tif (previous != null && (branch !== previous.branch || detached !== previous.detached)) {\n\t\t\tfields |= baseField;\n\t\t\tdirtyRead ??= readDirty(repo, signal);\n\t\t}\n\t}\n\n\tlet base = previous?.base;\n\tlet dependsOn = previous?.baseDependsOn ?? emptyRefs;\n\tlet baseKeys = previousBaseKeys;\n\tlet baseKeysRead = false;\n\tlet baseFailed = false;\n\tlet baseError: unknown;\n\tconst baseBranch = !detached && repo.branches.getAheadOfBase != null ? branch : undefined;\n\tif (fields & baseKeysField && !(fields & baseField) && baseBranch != null) {\n\t\tbaseKeys = await readBaseKeys(repo, baseBranch);\n\t\tbaseKeysRead = true;\n\t\tif (baseKeys == null || previousBaseKeys == null || !baseKeysEqual(baseKeys, previousBaseKeys)) {\n\t\t\tfields |= baseField;\n\t\t}\n\t}\n\tif (fields & baseField) {\n\t\tif (baseBranch == null) {\n\t\t\tbase = undefined;\n\t\t\tdependsOn = emptyRefs;\n\t\t\tbaseKeys = undefined;\n\t\t} else {\n\t\t\t// Snapshot before the base read, so a write it misses is one the next `gk` config event sees as a change\n\t\t\tif (!baseKeysRead) {\n\t\t\t\tbaseKeys = await readBaseKeys(repo, baseBranch);\n\t\t\t}\n\t\t\ttry {\n\t\t\t\tconst aheadOfBase = await repo.branches.getAheadOfBase?.(baseBranch, signal);\n\t\t\t\tbase = aheadOfBase?.base;\n\t\t\t\tdependsOn = aheadOfBase?.dependsOn ?? emptyRefs;\n\t\t\t} catch (ex) {\n\t\t\t\tif (isCancellationError(ex)) throw ex;\n\n\t\t\t\tbaseFailed = true;\n\t\t\t\tbaseError = ex;\n\t\t\t\t// Keep `baseDependsOn` for the same branch: a fetch moving one of its refs is what retries the base\n\t\t\t\tconst sameBranch = previous != null && branch === previous.branch && detached === previous.detached;\n\t\t\t\tbase = sameBranch ? previous.base : undefined;\n\t\t\t\tdependsOn = sameBranch ? previous.baseDependsOn : emptyRefs;\n\t\t\t\tbaseKeys = undefined;\n\t\t\t}\n\t\t}\n\t}\n\n\tconst dirty = dirtyRead != null ? await dirtyRead : (previous?.dirty ?? false);\n\n\treturn {\n\t\tsummary: {\n\t\t\trepoPath: key,\n\t\t\tbranch: branch,\n\t\t\tdetached: detached,\n\t\t\tsha: sha,\n\t\t\tupstream: upstream,\n\t\t\tahead: ahead,\n\t\t\tbehind: behind,\n\t\t\tdirty: dirty,\n\t\t\tbase: base,\n\t\t\tbaseDependsOn: dependsOn,\n\t\t\tstale: baseFailed,\n\t\t},\n\t\tbaseKeys: baseKeys,\n\t\tbaseError: baseError,\n\t};\n}\n\n/**\n * Reads {@link BaseKeys} for `branch`, normalized the way `getAheadOfBase` reads them. `undefined` when any read\n * failed — an unknown snapshot, so the next `gk` config change reads the base rather than trusting it.\n */\nasync function readBaseKeys(repo: RepositoryService, branch: string): Promise<BaseKeys | undefined> {\n\ttry {\n\t\tconst [user, detected, recorded] = await Promise.all([\n\t\t\trepo.branches.getStoredUserMergeTargetBranchName?.(branch),\n\t\t\trepo.branches.getStoredDetectedMergeTargetBranchName?.(branch),\n\t\t\trepo.config.getGkConfig?.(`branch.${branch}.gk-merge-base`),\n\t\t]);\n\t\treturn [user?.trim() || undefined, detected?.trim() || undefined, recorded?.trim() || undefined];\n\t} catch (ex) {\n\t\tif (isCancellationError(ex)) throw ex;\n\n\t\treturn undefined;\n\t}\n}\n\nfunction baseKeysEqual(a: BaseKeys, b: BaseKeys): boolean {\n\treturn a[0] === b[0] && a[1] === b[1] && a[2] === b[2];\n}\n\nfunction readDirty(repo: RepositoryService, signal: AbortSignal | undefined): Promise<boolean> {\n\tconst read = repo.status.hasWorkingChanges({ untracked: true }, signal);\n\t// Awaited only after the branch and base reads; if one of those throws first, this rejection has no other handler\n\tread.catch(noop);\n\treturn read;\n}\n\nfunction noop(): void {}\n\n/** Splits the upstream at the longest remote name it starts with; no split when none matches or nothing follows it */\nasync function toSummaryUpstream(\n\trepo: RepositoryService,\n\tupstream: GitTrackingUpstream,\n\tsignal: AbortSignal | undefined,\n): Promise<WorktreeSummaryUpstream> {\n\tconst name = upstream.name;\n\n\tlet remote: string | undefined;\n\tfor (const r of await repo.remotes.getRemotes(undefined, signal)) {\n\t\tif (r.name && name.startsWith(`${r.name}/`) && (remote == null || r.name.length > remote.length)) {\n\t\t\tremote = r.name;\n\t\t}\n\t}\n\n\tconst branch = remote != null ? name.substring(remote.length + 1) : undefined;\n\treturn {\n\t\tname: name,\n\t\tremote: branch ? remote : undefined,\n\t\tbranch: branch || undefined,\n\t\tmissing: upstream.missing,\n\t};\n}\n\nfunction summariesEqual(a: WorktreeSummary, b: WorktreeSummary): boolean {\n\treturn (\n\t\ta.repoPath === b.repoPath &&\n\t\ta.branch === b.branch &&\n\t\ta.detached === b.detached &&\n\t\ta.sha === b.sha &&\n\t\ta.ahead === b.ahead &&\n\t\ta.behind === b.behind &&\n\t\ta.dirty === b.dirty &&\n\t\ta.stale === b.stale &&\n\t\tareEqual(a.baseDependsOn, b.baseDependsOn) &&\n\t\t(a.upstream === b.upstream ||\n\t\t\t(a.upstream != null &&\n\t\t\t\tb.upstream != null &&\n\t\t\t\ta.upstream.name === b.upstream.name &&\n\t\t\t\ta.upstream.remote === b.upstream.remote &&\n\t\t\t\ta.upstream.branch === b.upstream.branch &&\n\t\t\t\ta.upstream.missing === b.upstream.missing)) &&\n\t\t(a.base === b.base ||\n\t\t\t(a.base != null &&\n\t\t\t\tb.base != null &&\n\t\t\t\ta.base.ref === b.base.ref &&\n\t\t\t\ta.base.ahead === b.base.ahead &&\n\t\t\t\ta.base.sha === b.base.sha))\n\t);\n}\n"],"mappings":"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"}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
+
import { Event } from "../utils/event.js";
|
|
1
2
|
import { Uri } from "../utils/uri.js";
|
|
2
3
|
import { GitProviderDescriptor } from "../git/providers/types.js";
|
|
3
4
|
import { GitIgnoreFilter } from "../git/watching/gitIgnoreFilter.js";
|
|
4
5
|
import { RepositoryChange } from "../git/models/repository.js";
|
|
5
6
|
import { GitServiceContext } from "../git/context.js";
|
|
6
|
-
import { Cache, CachedGitTypes } from "../git/cache.js";
|
|
7
|
+
import { Cache, CachedGitTypes, MergeTargetsChangeEvent } from "../git/cache.js";
|
|
7
8
|
import { GitOperationRunOptions } from "../git/providers/operations.js";
|
|
8
9
|
import { GitProvider } from "../git/providers/provider.js";
|
|
9
10
|
import { FilteredGitFeatures, GitFeatureOrPrefix, GitFeatures } from "../git/features.js";
|
|
@@ -86,6 +87,7 @@ export declare class CliGitProvider implements GitProvider {
|
|
|
86
87
|
constructor(options: CliGitProviderOptions);
|
|
87
88
|
/** The underlying cache instance (for repo path registration, etc.) */
|
|
88
89
|
get cache(): Cache;
|
|
90
|
+
get onDidChangeMergeTargets(): Event<MergeTargetsChangeEvent>;
|
|
89
91
|
/** The underlying git execution instance (for sub-providers that need direct exec/stream access) */
|
|
90
92
|
get git(): Git;
|
|
91
93
|
/** Returns whether the installed git version supports the given feature. */
|
|
@@ -177,6 +179,9 @@ export declare class CliGitProvider implements GitProvider {
|
|
|
177
179
|
getIgnoredUrisFilter(repoPath: string): Promise<(uri: Uri) => boolean>;
|
|
178
180
|
getLastFetchedTimestamp(repoPath: string): Promise<number | undefined>;
|
|
179
181
|
getIgnoreFilter(repoPath: string): GitIgnoreFilter;
|
|
182
|
+
getIgnoredDirectories(repoPath: string, cancellation?: AbortSignal): Promise<string[]>;
|
|
183
|
+
/** Every `.gitignore` git reads in the working tree (tracked, or untracked and not itself ignored), root included */
|
|
184
|
+
private getGitIgnorePaths;
|
|
180
185
|
private isTrackedWithDetails;
|
|
181
186
|
dispose(): void;
|
|
182
187
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { iterateByDelimiter } from "../utils/string.js";
|
|
1
2
|
import { getScopedLogger } from "../utils/logger.scoped.js";
|
|
2
3
|
import { fileUri, joinUriPath, parseUri, toFsPath } from "../utils/uri.js";
|
|
3
4
|
import { joinPaths, maybeUri, normalizePath, splitPath } from "../utils/path.js";
|
|
@@ -12,7 +13,7 @@ import { BlameGitSubProvider } from "./providers/blame.js";
|
|
|
12
13
|
import { BranchesGitSubProvider } from "./providers/branches.js";
|
|
13
14
|
import { StashGitSubProvider } from "./providers/stash.js";
|
|
14
15
|
import { CommitsGitSubProvider } from "./providers/commits.js";
|
|
15
|
-
import { ConfigGitSubProvider } from "./providers/config.js";
|
|
16
|
+
import { ConfigGitSubProvider, isGitBooleanFalse, parseGitBoolean } from "./providers/config.js";
|
|
16
17
|
import { ContributorsGitSubProvider } from "./providers/contributors.js";
|
|
17
18
|
import { DiffGitSubProvider } from "./providers/diff.js";
|
|
18
19
|
import { GraphGitSubProvider } from "./providers/graph.js";
|
|
@@ -28,7 +29,9 @@ import { StatusGitSubProvider } from "./providers/status.js";
|
|
|
28
29
|
import { TagsGitSubProvider } from "./providers/tags.js";
|
|
29
30
|
import { WorktreesGitSubProvider } from "./providers/worktrees.js";
|
|
30
31
|
import { promises } from "node:fs";
|
|
32
|
+
import { homedir } from "node:os";
|
|
31
33
|
import * as path from "node:path";
|
|
34
|
+
import { env } from "node:process";
|
|
32
35
|
//#region ../git-cli/src/cliGitProvider.ts
|
|
33
36
|
/**
|
|
34
37
|
* A fully-wired CLI-based git provider that executes git commands via `child_process`.
|
|
@@ -90,6 +93,9 @@ var CliGitProvider = class {
|
|
|
90
93
|
get cache() {
|
|
91
94
|
return this._cache;
|
|
92
95
|
}
|
|
96
|
+
get onDidChangeMergeTargets() {
|
|
97
|
+
return this._cache.onDidChangeMergeTargets;
|
|
98
|
+
}
|
|
93
99
|
/** The underlying git execution instance (for sub-providers that need direct exec/stream access) */
|
|
94
100
|
get git() {
|
|
95
101
|
return this._git;
|
|
@@ -327,12 +333,75 @@ var CliGitProvider = class {
|
|
|
327
333
|
repoPath,
|
|
328
334
|
gitDirPath,
|
|
329
335
|
fs: this.context.fs,
|
|
330
|
-
getGlobalExcludesPath: this.config.getConfig != null ? async () =>
|
|
336
|
+
getGlobalExcludesPath: this.config.getConfig != null ? async () => {
|
|
337
|
+
const configured = await this.config.getConfig(repoPath, "core.excludesFile");
|
|
338
|
+
return configured != null ? resolveGitPathConfig(configured, repoPath) : getDefaultGlobalExcludesPath();
|
|
339
|
+
} : void 0,
|
|
340
|
+
getInfoExcludePath: async () => {
|
|
341
|
+
const gitDir = await this.config.getGitDir?.(repoPath);
|
|
342
|
+
if (gitDir == null) return void 0;
|
|
343
|
+
return `${(gitDir.commonUri ?? gitDir.uri).fsPath}/info/exclude`;
|
|
344
|
+
},
|
|
345
|
+
getGitIgnorePaths: () => this.getGitIgnorePaths(repoPath),
|
|
346
|
+
getIgnoreCase: async () => {
|
|
347
|
+
const value = await this.config.getConfig?.(repoPath, "core.ignorecase");
|
|
348
|
+
if (value == null) return void 0;
|
|
349
|
+
if (parseGitBoolean(value)) return true;
|
|
350
|
+
return isGitBooleanFalse(value) ? false : void 0;
|
|
351
|
+
}
|
|
331
352
|
});
|
|
332
353
|
this._cache.gitIgnore.set(repoPath, filter);
|
|
333
354
|
}
|
|
334
355
|
return filter;
|
|
335
356
|
}
|
|
357
|
+
async getIgnoredDirectories(repoPath, cancellation) {
|
|
358
|
+
const listed = await this._git.run({
|
|
359
|
+
cwd: repoPath,
|
|
360
|
+
errors: "throw",
|
|
361
|
+
cancellation
|
|
362
|
+
}, "ls-files", "-z", "--others", "--ignored", "--exclude-standard", "--directory");
|
|
363
|
+
const candidates = [];
|
|
364
|
+
for (const entry of iterateByDelimiter(listed.stdout, "\0")) if (entry.length > 1 && entry.endsWith("/")) candidates.push(entry);
|
|
365
|
+
if (!candidates.length) return [];
|
|
366
|
+
const confirmed = await this._git.run({
|
|
367
|
+
cwd: repoPath,
|
|
368
|
+
errors: "throw",
|
|
369
|
+
cancellation,
|
|
370
|
+
stdin: `${candidates.join("\0")}\0`,
|
|
371
|
+
expectedExitCodes: [1]
|
|
372
|
+
}, "check-ignore", "-z", "--stdin");
|
|
373
|
+
const directories = [];
|
|
374
|
+
for (const entry of iterateByDelimiter(confirmed.stdout, "\0")) {
|
|
375
|
+
if (entry.length < 2 || !entry.endsWith("/") || /[*?[\]{}()!+@\\]/.test(entry)) continue;
|
|
376
|
+
const directory = entry.substring(0, entry.length - 1);
|
|
377
|
+
if (directory === ".git") continue;
|
|
378
|
+
directories.push(directory);
|
|
379
|
+
}
|
|
380
|
+
directories.sort();
|
|
381
|
+
const kept = /* @__PURE__ */ new Set();
|
|
382
|
+
for (const directory of directories) {
|
|
383
|
+
let beneathKept = false;
|
|
384
|
+
for (let i = directory.indexOf("/"); i !== -1; i = directory.indexOf("/", i + 1)) if (kept.has(directory.substring(0, i))) {
|
|
385
|
+
beneathKept = true;
|
|
386
|
+
break;
|
|
387
|
+
}
|
|
388
|
+
if (!beneathKept) kept.add(directory);
|
|
389
|
+
}
|
|
390
|
+
return [...kept];
|
|
391
|
+
}
|
|
392
|
+
/** Every `.gitignore` git reads in the working tree (tracked, or untracked and not itself ignored), root included */
|
|
393
|
+
async getGitIgnorePaths(repoPath) {
|
|
394
|
+
const result = await this._git.run({
|
|
395
|
+
cwd: repoPath,
|
|
396
|
+
errors: "throw"
|
|
397
|
+
}, "ls-files", "-z", "--cached", "--others", "--exclude-standard", "--", ":(glob)**/.gitignore");
|
|
398
|
+
const paths = [];
|
|
399
|
+
for (const line of iterateByDelimiter(result.stdout, "\0")) {
|
|
400
|
+
if (!line.length) continue;
|
|
401
|
+
paths.push(line);
|
|
402
|
+
}
|
|
403
|
+
return paths;
|
|
404
|
+
}
|
|
336
405
|
async isTrackedWithDetails(filePath, repoPath, rev) {
|
|
337
406
|
if (rev === "0000000000000000000000000000000000000000-") return void 0;
|
|
338
407
|
const scope = getScopedLogger();
|
|
@@ -355,6 +424,22 @@ var CliGitProvider = class {
|
|
|
355
424
|
if (this._cacheOwned) this._cache.dispose();
|
|
356
425
|
}
|
|
357
426
|
};
|
|
427
|
+
/**
|
|
428
|
+
* Resolves a pathname config value the way git does: a leading `~/` (or a bare `~`) is the home directory, and a
|
|
429
|
+
* relative path is taken from the repository root. Done here rather than with `git config --type=path`, which needs
|
|
430
|
+
* git 2.18, newer than the minimum supported version.
|
|
431
|
+
*/
|
|
432
|
+
function resolveGitPathConfig(value, repoPath) {
|
|
433
|
+
if (value === "~" || value.startsWith("~/")) return path.join(env.HOME || homedir(), value.substring(1));
|
|
434
|
+
return path.resolve(repoPath, value);
|
|
435
|
+
}
|
|
436
|
+
/** Where git reads global excludes when `core.excludesFile` is unset: `$XDG_CONFIG_HOME/git/ignore`, else `$HOME/.config/git/ignore` */
|
|
437
|
+
function getDefaultGlobalExcludesPath() {
|
|
438
|
+
const xdgConfigHome = env.XDG_CONFIG_HOME;
|
|
439
|
+
if (xdgConfigHome) return path.join(xdgConfigHome, "git", "ignore");
|
|
440
|
+
const home = env.HOME || homedir();
|
|
441
|
+
return home ? path.join(home, ".config", "git", "ignore") : void 0;
|
|
442
|
+
}
|
|
358
443
|
//#endregion
|
|
359
444
|
export { CliGitProvider };
|
|
360
445
|
|