Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/copilot/src/platform/git/vscode-node/gitServiceImpl.ts
13401 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import * as vscode from 'vscode';
7
8
import { execFile } from 'child_process';
9
import { promisify } from 'util';
10
import { Uri } from 'vscode';
11
import { BatchedProcessor } from '../../../util/common/async';
12
import { coalesce } from '../../../util/vs/base/common/arrays';
13
import { Sequencer } from '../../../util/vs/base/common/async';
14
import { CachedFunction } from '../../../util/vs/base/common/cache';
15
import { CancellationToken, cancelOnDispose } from '../../../util/vs/base/common/cancellation';
16
import { Emitter, Event } from '../../../util/vs/base/common/event';
17
import { Disposable } from '../../../util/vs/base/common/lifecycle';
18
import { autorun, IObservable, observableFromEvent, observableSignalFromEvent, observableValue, waitForState } from '../../../util/vs/base/common/observableInternal';
19
import * as path from '../../../util/vs/base/common/path';
20
import { isEqual } from '../../../util/vs/base/common/resources';
21
import { URI } from '../../../util/vs/base/common/uri';
22
import { ILogService } from '../../log/common/logService';
23
import { IGitExtensionService } from '../common/gitExtensionService';
24
import { IGitService, RepoContext } from '../common/gitService';
25
import { parseGitRemotes } from '../common/utils';
26
import { API, APIState, Branch, Change, CommitOptions, CommitShortStat, DiffChange, Ref, RefQuery, Repository, RepositoryAccessDetails } from '../vscode/git';
27
28
const execFileAsync = promisify(execFile);
29
30
export class GitServiceImpl extends Disposable implements IGitService {
31
32
declare readonly _serviceBrand: undefined;
33
34
readonly activeRepository = observableValue<RepoContext | undefined>(this, undefined);
35
36
private readonly _getRepositorySequencer = new Sequencer();
37
38
private _onDidOpenRepository = new Emitter<RepoContext>();
39
readonly onDidOpenRepository: Event<RepoContext> = this._onDidOpenRepository.event;
40
private _onDidCloseRepository = new Emitter<RepoContext>();
41
readonly onDidCloseRepository: Event<RepoContext> = this._onDidCloseRepository.event;
42
private _onDidFinishInitialRepositoryDiscovery = new Emitter<void>();
43
readonly onDidFinishInitialization: Event<void> = this._onDidFinishInitialRepositoryDiscovery.event;
44
private _isInitialized = observableValue(this, false);
45
constructor(
46
@IGitExtensionService private readonly gitExtensionService: IGitExtensionService,
47
@ILogService private readonly logService: ILogService
48
) {
49
super();
50
51
this._register(this._onDidOpenRepository);
52
this._register(this._onDidCloseRepository);
53
this._register(this._onDidFinishInitialRepositoryDiscovery);
54
55
const gitAPI = this.gitExtensionService.getExtensionApi();
56
if (gitAPI) {
57
this.registerGitAPIListeners(gitAPI);
58
} else {
59
this._register(this.gitExtensionService.onDidChange((status) => {
60
if (status.enabled) {
61
const gitAPI = this.gitExtensionService.getExtensionApi();
62
if (gitAPI) {
63
this.registerGitAPIListeners(gitAPI);
64
return;
65
}
66
}
67
68
// Extension is disabled / git is not available so we say all repositories are discovered
69
this._onDidFinishInitialRepositoryDiscovery.fire();
70
this._isInitialized.set(true, undefined);
71
}));
72
}
73
}
74
75
private registerGitAPIListeners(gitAPI: API) {
76
this._register(gitAPI.onDidOpenRepository(repository => this.doOpenRepository(repository)));
77
this._register(gitAPI.onDidCloseRepository(repository => this.doCloseRepository(repository)));
78
79
for (const repository of gitAPI.repositories) {
80
this.doOpenRepository(repository);
81
}
82
83
// Initial repository discovery
84
const stateObs = observableFromEvent(this,
85
gitAPI.onDidChangeState as Event<APIState>, () => gitAPI.state);
86
87
this._register(autorun(async reader => {
88
const state = stateObs.read(reader);
89
if (state !== 'initialized') {
90
return;
91
}
92
93
// Wait for all discovered repositories to be initialized
94
await Promise.all(gitAPI.repositories.map(repository => {
95
const HEAD = observableFromEvent(this, repository.state.onDidChange as Event<void>, () => repository.state.HEAD);
96
return waitForState(HEAD, state => state !== undefined, undefined, cancelOnDispose(this._store));
97
}));
98
99
this._isInitialized.set(true, undefined);
100
this._onDidFinishInitialRepositoryDiscovery.fire();
101
102
this.logService.trace(`[GitServiceImpl] Initial repository discovery finished: ${this.repositories.length} repositories found.`);
103
}));
104
}
105
106
get isInitialized(): boolean {
107
return this._isInitialized.get();
108
}
109
110
public getRecentRepositories(): Iterable<RepositoryAccessDetails> {
111
const gitAPI = this.gitExtensionService.getExtensionApi();
112
if (!gitAPI) {
113
return [];
114
}
115
return gitAPI.recentRepositories;
116
}
117
118
async initRepository(uri: URI): Promise<Repository | undefined> {
119
const gitAPI = this.gitExtensionService.getExtensionApi();
120
const repository = await gitAPI?.init(uri);
121
if (!repository) {
122
return undefined;
123
}
124
125
await this.waitForRepositoryState(repository);
126
return repository;
127
}
128
129
async openRepository(uri: URI): Promise<Repository | undefined> {
130
const repository = await this._getRepository(uri, true);
131
if (!repository) {
132
return undefined;
133
}
134
135
await this.waitForRepositoryState(repository);
136
return repository;
137
}
138
139
async getRepository2(uri: URI): Promise<Repository | undefined> {
140
const repository = await this._getRepository(uri, false);
141
return repository;
142
}
143
144
async getRepository(uri: URI, forceOpen = true): Promise<RepoContext | undefined> {
145
const repository = await this._getRepository(uri, forceOpen);
146
if (!repository) {
147
return undefined;
148
}
149
150
await this.waitForRepositoryState(repository);
151
return GitServiceImpl.repoToRepoContext(repository);
152
}
153
154
private async _getRepository(uri: URI, forceOpen = true): Promise<Repository | undefined> {
155
return this._getRepositorySequencer.queue(async () => {
156
const gitAPI = this.gitExtensionService.getExtensionApi();
157
if (!gitAPI) {
158
return undefined;
159
}
160
161
if (!(uri instanceof vscode.Uri)) {
162
// The git extension API expects a vscode.Uri, so we convert it if necessary
163
uri = vscode.Uri.parse(uri.toString());
164
}
165
166
// Ensure that the initial
167
// repository discovery is
168
// finished
169
await this.initialize();
170
171
// Query opened repositories
172
let repository = gitAPI.getRepository(uri);
173
if (repository) {
174
return repository;
175
}
176
177
if (!forceOpen) {
178
return undefined;
179
}
180
181
// Open repository
182
repository = await gitAPI.openRepository(uri);
183
if (!repository) {
184
return undefined;
185
}
186
187
return repository;
188
});
189
}
190
191
async getRepositoryFetchUrls(uri: URI): Promise<Pick<RepoContext, 'rootUri' | 'remoteFetchUrls'> | undefined> {
192
this.logService.trace(`[GitServiceImpl][getRepositoryFetchUrls] URI: ${uri.toString()}`);
193
194
const gitAPI = this.gitExtensionService.getExtensionApi();
195
if (!gitAPI) {
196
return undefined;
197
}
198
199
// Query opened repositories
200
const repository = gitAPI.getRepository(uri);
201
if (repository) {
202
await this.waitForRepositoryState(repository);
203
204
const remotes = {
205
rootUri: repository.rootUri,
206
remoteFetchUrls: repository.state.remotes.map(r => r.fetchUrl),
207
};
208
209
this.logService.trace(`[GitServiceImpl][getRepositoryFetchUrls] Remotes (open repository): ${JSON.stringify(remotes)}`);
210
return remotes;
211
}
212
213
try {
214
const uriStat = await vscode.workspace.fs.stat(uri);
215
if (uriStat.type !== vscode.FileType.Directory) {
216
uri = URI.file(path.dirname(uri.fsPath));
217
}
218
219
// Get repository root
220
const repositoryRoot = await gitAPI.getRepositoryRoot(uri);
221
if (!repositoryRoot) {
222
this.logService.trace(`[GitServiceImpl][getRepositoryFetchUrls] No repository root found`);
223
return undefined;
224
}
225
226
this.logService.trace(`[GitServiceImpl][getRepositoryFetchUrls] Repository root: ${repositoryRoot.toString()}`);
227
const buffer = await vscode.workspace.fs.readFile(URI.file(path.join(repositoryRoot.fsPath, '.git', 'config')));
228
229
const remotes = {
230
rootUri: repositoryRoot,
231
remoteFetchUrls: parseGitRemotes(buffer.toString()).map(remote => remote.fetchUrl)
232
};
233
234
this.logService.trace(`[GitServiceImpl][getRepositoryFetchUrls] Remotes (.git/config): ${JSON.stringify(remotes)}`);
235
return remotes;
236
} catch (error) {
237
this.logService.error(`[GitServiceImpl][getRepositoryFetchUrls] Failed to read remotes from .git/config: ${error.message}`);
238
return undefined;
239
}
240
}
241
242
async add(uri: URI, paths: string[]): Promise<void> {
243
const gitAPI = this.gitExtensionService.getExtensionApi();
244
const repository = gitAPI?.getRepository(uri);
245
await repository?.add(paths);
246
}
247
248
async restore(uri: URI, paths: string[], options?: { staged?: boolean; ref?: string }): Promise<void> {
249
const gitAPI = this.gitExtensionService.getExtensionApi();
250
const repository = gitAPI?.getRepository(uri);
251
await repository?.restore(paths, options);
252
}
253
254
async diffBetweenPatch(uri: vscode.Uri, ref1: string, ref2: string, path?: string): Promise<string | undefined> {
255
const gitAPI = this.gitExtensionService.getExtensionApi();
256
const repository = gitAPI?.getRepository(uri);
257
return repository?.diffBetweenPatch(ref1, ref2, path);
258
}
259
260
async diffBetweenWithStats(uri: vscode.Uri, ref1: string, ref2: string, path?: string): Promise<DiffChange[] | undefined> {
261
const gitAPI = this.gitExtensionService.getExtensionApi();
262
const repository = gitAPI?.getRepository(uri);
263
return await repository?.diffBetweenWithStats(ref1, ref2, path);
264
}
265
266
async diffWith(uri: vscode.Uri, ref: string): Promise<Change[] | undefined> {
267
const gitAPI = this.gitExtensionService.getExtensionApi();
268
const repository = gitAPI?.getRepository(uri);
269
return repository?.diffWith(ref);
270
}
271
272
async diffIndexWithHEADShortStats(uri: URI): Promise<CommitShortStat | undefined> {
273
const gitAPI = this.gitExtensionService.getExtensionApi();
274
const repository = gitAPI?.getRepository(uri);
275
if (!repository?.diffIndexWithHEADShortStats) {
276
return undefined;
277
}
278
return await repository?.diffIndexWithHEADShortStats(uri.fsPath);
279
}
280
281
async getMergeBase(uri: URI, ref1: string, ref2: string): Promise<string | undefined> {
282
const gitAPI = this.gitExtensionService.getExtensionApi();
283
const repository = gitAPI?.getRepository(uri);
284
return repository?.getMergeBase(ref1, ref2);
285
}
286
287
async commit(uri: URI, message: string, opts?: CommitOptions): Promise<void> {
288
const gitAPI = this.gitExtensionService.getExtensionApi();
289
const repository = gitAPI?.getRepository(uri);
290
if (!repository) {
291
return;
292
}
293
294
await repository.commit(message, opts);
295
}
296
297
async applyPatch(uri: URI, patch: string): Promise<void> {
298
const gitAPI = this.gitExtensionService.getExtensionApi();
299
const repository = gitAPI?.getRepository(uri);
300
return await repository?.apply(patch, false);
301
}
302
303
async rebase(uri: URI, branch: string): Promise<void> {
304
try {
305
const gitAPI = this.gitExtensionService.getExtensionApi();
306
const repository = gitAPI?.getRepository(uri);
307
await repository?.rebase(branch);
308
} catch (error) {
309
this.logService.error(`[GitServiceImpl][rebase] Failed to rebase ${uri.toString()} on ${branch}: ${error.message}`);
310
}
311
}
312
313
async createWorktree(uri: URI, options?: { path?: string; commitish?: string; branch?: string; noTrack?: boolean }): Promise<string | undefined> {
314
const gitAPI = this.gitExtensionService.getExtensionApi();
315
const repository = gitAPI?.getRepository(uri);
316
return await repository?.createWorktree(options);
317
}
318
319
async deleteWorktree(uri: URI, path: string, options?: { force?: boolean }): Promise<void> {
320
const gitAPI = this.gitExtensionService.getExtensionApi();
321
const repository = gitAPI?.getRepository(uri);
322
return await repository?.deleteWorktree(path, options);
323
}
324
325
async migrateChanges(uri: URI, sourceRepositoryUri: URI, options?: { confirmation?: boolean; deleteFromSource?: boolean; untracked?: boolean }): Promise<void> {
326
const gitAPI = this.gitExtensionService.getExtensionApi();
327
const repository = gitAPI?.getRepository(uri);
328
return await repository?.migrateChanges(sourceRepositoryUri.fsPath, options);
329
}
330
331
async getBranch(uri: URI, name: string): Promise<Branch | undefined> {
332
const gitAPI = this.gitExtensionService.getExtensionApi();
333
const repository = gitAPI?.getRepository(uri);
334
return await repository?.getBranch(name);
335
}
336
337
async getBranchBase(uri: URI, name: string): Promise<Branch | undefined> {
338
const gitAPI = this.gitExtensionService.getExtensionApi();
339
const repository = gitAPI?.getRepository(uri);
340
return await repository?.getBranchBase(name);
341
}
342
343
async getRefs(uri: URI, query: RefQuery, cancellationToken?: CancellationToken): Promise<Ref[]> {
344
const gitAPI = this.gitExtensionService.getExtensionApi();
345
const repository = gitAPI?.getRepository(uri);
346
return await repository?.getRefs(query, cancellationToken) ?? [];
347
}
348
349
async isBranchProtected(uri: URI, branch?: string | Branch): Promise<boolean | undefined> {
350
try {
351
const gitAPI = this.gitExtensionService.getExtensionApi();
352
const repository = gitAPI?.getRepository(uri);
353
if (!repository) {
354
return undefined;
355
}
356
357
const branchToCheck = typeof branch === 'string'
358
? await repository.getBranch(branch)
359
: branch;
360
return repository.isBranchProtected(branchToCheck);
361
} catch (error) {
362
const branchLabel = typeof branch === 'string' ? branch : branch?.name;
363
this.logService.error(`[GitServiceImpl][isBranchProtected] Failed to check branch protection for ${uri.toString()}${branchLabel ? ` (${branchLabel})` : ''}: ${error instanceof Error ? error.message : String(error)}`);
364
return undefined;
365
}
366
}
367
368
async generateRandomBranchName(uri: URI): Promise<string | undefined> {
369
try {
370
const gitAPI = this.gitExtensionService.getExtensionApi();
371
const repository = gitAPI?.getRepository(uri);
372
373
const branchName = await repository?.generateRandomBranchName();
374
return branchName;
375
} catch (error) {
376
this.logService.error(`[GitServiceImpl][generateRandomBranchName] Failed to generate random branch name: ${error instanceof Error ? error.message : String(error)}`);
377
return undefined;
378
}
379
}
380
381
async exec(cwd: URI, args: string[], env?: Record<string, string>): Promise<string> {
382
const gitAPI = this.gitExtensionService.getExtensionApi();
383
const gitPath = gitAPI?.git.path ?? 'git';
384
const gitEnv = Object.assign({}, process.env, env, {
385
GIT_AUTHOR_NAME: 'VS Code',
386
GIT_AUTHOR_EMAIL: '[email protected]',
387
GIT_COMMITTER_NAME: 'VS Code',
388
GIT_COMMITTER_EMAIL: '[email protected]',
389
LANG: 'en_US.UTF-8',
390
LANGUAGE: 'en',
391
LC_ALL: 'en_US.UTF-8'
392
} satisfies Record<string, string>);
393
394
const timer = performance.now();
395
396
try {
397
const result = await execFileAsync(gitPath, args, {
398
cwd: cwd.fsPath,
399
encoding: 'utf8',
400
env: gitEnv
401
});
402
403
if (result.stderr) {
404
this.logService.error(`[GitServiceImpl][exec] git ${args.join(' ')} [${Math.round(performance.now() - timer)}ms] Error: ${result.stderr}`);
405
throw new Error(`Failed to execute git command (git ${args.join(' ')}). Error: ${result.stderr}`);
406
}
407
408
this.logService.trace(`[GitServiceImpl][exec] git ${args.join(' ')} [${Math.round(performance.now() - timer)}ms]`);
409
return result.stdout.trim();
410
} catch (error) {
411
const errorMessage = error instanceof Error ? error.message : String(error);
412
this.logService.error(`[GitServiceImpl][exec] git ${args.join(' ')} [${Math.round(performance.now() - timer)}ms] Error: ${errorMessage}`);
413
414
throw new Error(`Failed to execute git command (git ${args.join(' ')}). Error: ${errorMessage}`);
415
}
416
}
417
418
async initialize(): Promise<void> {
419
if (this._isInitialized.get()) {
420
return;
421
}
422
423
await waitForState(this._isInitialized, state => state, undefined, cancelOnDispose(this._store));
424
425
if (this.repositories.length > 0) {
426
await waitForState(this.activeRepository, state => state !== undefined, undefined, cancelOnDispose(this._store));
427
}
428
}
429
430
private async doOpenRepository(repository: Repository): Promise<void> {
431
this.logService.trace(`[GitServiceImpl][doOpenRepository] Repository: ${repository.rootUri.toString()}`);
432
433
// The `gitAPI.onDidOpenRepository` event is fired before `git status` completes and the repository
434
// state is initialized. `IGitService.onDidOpenRepository` will only fire after the repository state
435
// is initialized.
436
const HEAD = observableFromEvent(this, repository.state.onDidChange as Event<void>, () => repository.state.HEAD);
437
await waitForState(HEAD, state => state !== undefined, undefined, cancelOnDispose(this._store));
438
439
this.logService.trace(`[GitServiceImpl][doOpenRepository] Repository initialized: ${JSON.stringify(HEAD.get())}`);
440
441
// Active repository
442
const selectedObs = observableFromEvent(this,
443
repository.ui.onDidChange as Event<void>, () => repository.ui.selected);
444
445
const onDidChangeStateSignal = observableSignalFromEvent(this, repository.state.onDidChange as Event<void>);
446
447
this._register(autorun(reader => {
448
onDidChangeStateSignal.read(reader);
449
const selected = selectedObs.read(reader);
450
451
// eslint-disable-next-line local/code-no-observable-get-in-reactive-context
452
const activeRepository = this.activeRepository.get();
453
if (activeRepository && !selected && !isEqual(activeRepository.rootUri, repository.rootUri)) {
454
return;
455
}
456
457
const repositoryContext = GitServiceImpl.repoToRepoContext(repository);
458
this.logService.trace(`[GitServiceImpl][doOpenRepository] Active repository: ${JSON.stringify(repositoryContext)}`);
459
this.activeRepository.set(repositoryContext, undefined);
460
}));
461
462
// Open repository event
463
const repositoryContext = GitServiceImpl.repoToRepoContext(repository);
464
if (repositoryContext) {
465
this._onDidOpenRepository.fire(repositoryContext);
466
}
467
}
468
469
private doCloseRepository(repository: Repository): void {
470
this.logService.trace(`[GitServiceImpl][doCloseRepository] Repository: ${repository.rootUri.toString()}`);
471
472
const repositoryContext = GitServiceImpl.repoToRepoContext(repository);
473
if (repositoryContext) {
474
this._onDidCloseRepository.fire(repositoryContext);
475
}
476
}
477
478
private async waitForRepositoryState(repository: Repository): Promise<void> {
479
if (repository.state.HEAD) {
480
return;
481
}
482
483
const HEAD = observableFromEvent(this, repository.state.onDidChange as Event<void>, () => repository.state.HEAD);
484
await waitForState(HEAD, state => state !== undefined, undefined, cancelOnDispose(this._store));
485
}
486
487
private static repoToRepoContext(repo: Repository): RepoContext;
488
private static repoToRepoContext(repo: Repository | undefined | null): RepoContext | undefined;
489
private static repoToRepoContext(repo: Repository | undefined | null): RepoContext | undefined {
490
if (!repo) {
491
return undefined;
492
}
493
494
return new RepoContextImpl(repo);
495
}
496
497
get repositories(): RepoContext[] {
498
const gitAPI = this.gitExtensionService.getExtensionApi();
499
if (!gitAPI) {
500
return [];
501
}
502
503
return coalesce(gitAPI.repositories
504
.filter(repository => repository.state.HEAD !== undefined)
505
.map(repository => GitServiceImpl.repoToRepoContext(repository)));
506
}
507
}
508
509
export class RepoContextImpl implements RepoContext {
510
public readonly rootUri = this._repo.rootUri;
511
public readonly kind = this._repo.kind;
512
public readonly isUsingVirtualFileSystem = this._repo.isUsingVirtualFileSystem;
513
public readonly headBranchName = this._repo.state.HEAD?.name;
514
public readonly headCommitHash = this._repo.state.HEAD?.commit;
515
public readonly headIncomingChanges = this._repo.state.HEAD?.behind;
516
public readonly headOutgoingChanges = this._repo.state.HEAD?.ahead;
517
public readonly upstreamBranchName = this._repo.state.HEAD?.upstream?.name;
518
public readonly upstreamRemote = this._repo.state.HEAD?.upstream?.remote;
519
public readonly isRebasing = this._repo.state.rebaseCommit !== null;
520
public readonly remotes = this._repo.state.remotes.map(r => r.name);
521
public readonly remoteFetchUrls = this._repo.state.remotes.map(r => r.fetchUrl);
522
public readonly worktrees = this._repo.state.worktrees;
523
524
public readonly changes = {
525
mergeChanges: this._repo.state.mergeChanges,
526
indexChanges: this._repo.state.indexChanges,
527
workingTree: this._repo.state.workingTreeChanges,
528
untrackedChanges: this._repo.state.untrackedChanges
529
};
530
531
private readonly _onDidChangeSignal = observableSignalFromEvent(this, this._repo.state.onDidChange as Event<void>);
532
533
public readonly headBranchNameObs: IObservable<string | undefined> = this._onDidChangeSignal.map(() => this._repo.state.HEAD?.name);
534
public readonly headCommitHashObs: IObservable<string | undefined> = this._onDidChangeSignal.map(() => this._repo.state.HEAD?.commit);
535
public readonly upstreamBranchNameObs: IObservable<string | undefined> = this._onDidChangeSignal.map(() => this._repo.state.HEAD?.upstream?.name);
536
public readonly upstreamRemoteObs: IObservable<string | undefined> = this._onDidChangeSignal.map(() => this._repo.state.HEAD?.upstream?.remote);
537
public readonly isRebasingObs: IObservable<boolean> = this._onDidChangeSignal.map(() => this._repo.state.rebaseCommit !== null);
538
539
private readonly _checkIsIgnored = new BatchedProcessor<string, boolean>(async (paths) => {
540
const result = await this._repo.checkIgnore(paths);
541
return paths.map(p => result.has(p));
542
}, 1000);
543
private readonly _isIgnored = new CachedFunction(async (documentUri: string) => {
544
const path = Uri.parse(documentUri).fsPath;
545
const result = await this._checkIsIgnored.request(path);
546
return result;
547
});
548
549
public isIgnored(uri: URI): Promise<boolean> {
550
return this._isIgnored.get(uri.toString());
551
}
552
553
constructor(
554
private readonly _repo: Repository
555
) {
556
}
557
}
558
559