Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/api/common/extHostGitExtensionService.ts
13397 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 type * as vscode from 'vscode';
7
import { Event } from '../../../base/common/event.js';
8
import { Disposable, DisposableMap } from '../../../base/common/lifecycle.js';
9
import { URI, UriComponents } from '../../../base/common/uri.js';
10
import { ExtensionIdentifier } from '../../../platform/extensions/common/extensions.js';
11
import { createDecorator } from '../../../platform/instantiation/common/instantiation.js';
12
import { IExtHostExtensionService } from './extHostExtensionService.js';
13
import { IExtHostRpcService } from './extHostRpcService.js';
14
import { ExtHostGitExtensionShape, GitBranchDto, GitChangeDto, GitDiffChangeDto, GitRefDto, GitRefQueryDto, GitRefTypeDto, GitRepositoryStateDto, GitUpstreamRefDto, MainContext, MainThreadGitExtensionShape } from './extHost.protocol.js';
15
import { ResourceMap } from '../../../base/common/map.js';
16
17
const GIT_EXTENSION_ID = 'vscode.git';
18
19
function toGitRefTypeDto(type: GitRefType): GitRefTypeDto {
20
switch (type) {
21
case GitRefType.Head: return GitRefTypeDto.Head;
22
case GitRefType.RemoteHead: return GitRefTypeDto.RemoteHead;
23
case GitRefType.Tag: return GitRefTypeDto.Tag;
24
default: throw new Error(`Unknown GitRefType: ${type}`);
25
}
26
}
27
28
function toGitBranchDto(branch: Branch): GitBranchDto {
29
return {
30
name: branch.name,
31
commit: branch.commit,
32
type: toGitRefTypeDto(branch.type),
33
remote: branch.remote,
34
upstream: branch.upstream ? toGitUpstreamRefDto(branch.upstream) : undefined,
35
ahead: branch.ahead,
36
behind: branch.behind,
37
};
38
}
39
40
function toGitUpstreamRefDto(upstream: UpstreamRef): GitUpstreamRefDto {
41
return {
42
remote: upstream.remote,
43
name: upstream.name,
44
commit: upstream.commit,
45
};
46
}
47
48
// Status values from the git extension's const enum Status
49
const enum GitStatus {
50
INDEX_ADDED = 1,
51
INDEX_DELETED = 2,
52
INDEX_RENAMED = 3,
53
MODIFIED = 5,
54
DELETED = 6,
55
UNTRACKED = 7,
56
INTENT_TO_ADD = 9,
57
INTENT_TO_RENAME = 10,
58
}
59
60
function toGitChangeDto(change: Change): GitChangeDto {
61
switch (change.status) {
62
// Added: no original
63
case GitStatus.INDEX_ADDED:
64
case GitStatus.UNTRACKED:
65
case GitStatus.INTENT_TO_ADD:
66
return { uri: change.uri, originalUri: undefined, modifiedUri: change.uri };
67
68
// Deleted: no modified
69
case GitStatus.INDEX_DELETED:
70
case GitStatus.DELETED:
71
return { uri: change.uri, originalUri: change.uri, modifiedUri: undefined };
72
73
// Renamed: original is old name, modified is new name
74
case GitStatus.INDEX_RENAMED:
75
case GitStatus.INTENT_TO_RENAME:
76
return { uri: change.uri, originalUri: change.originalUri, modifiedUri: change.renameUri };
77
78
// Modified and everything else: both original and modified
79
default:
80
return { uri: change.uri, originalUri: change.originalUri, modifiedUri: change.uri };
81
}
82
}
83
84
interface DiffChange extends Change {
85
readonly insertions: number;
86
readonly deletions: number;
87
}
88
89
interface Repository {
90
readonly rootUri: vscode.Uri;
91
readonly state: RepositoryState;
92
93
status(): Promise<void>;
94
getBranchBase(name: string): Promise<Branch | undefined>;
95
getRefs(query: GitRefQuery, token?: vscode.CancellationToken): Promise<GitRef[]>;
96
diffBetweenWithStats(ref1: string, ref2: string, path?: string): Promise<DiffChange[]>;
97
diffBetweenWithStats2(ref: string, path?: string): Promise<DiffChange[]>;
98
isBranchProtected(branch?: Branch): boolean;
99
}
100
101
interface Change {
102
readonly uri: vscode.Uri;
103
readonly originalUri: vscode.Uri;
104
readonly renameUri: vscode.Uri | undefined;
105
readonly status: number;
106
}
107
108
interface RepositoryState {
109
readonly HEAD: Branch | undefined;
110
readonly remotes: Remote[];
111
readonly mergeChanges: Change[];
112
readonly indexChanges: Change[];
113
readonly workingTreeChanges: Change[];
114
readonly untrackedChanges: Change[];
115
readonly onDidChange: Event<void>;
116
}
117
118
interface Remote {
119
readonly name: string;
120
readonly fetchUrl?: string;
121
readonly pushUrl?: string;
122
readonly isReadOnly: boolean;
123
}
124
125
interface Branch extends GitRef {
126
readonly base?: BaseRef;
127
readonly upstream?: UpstreamRef;
128
readonly ahead?: number;
129
readonly behind?: number;
130
}
131
132
interface BaseRef {
133
readonly name: string;
134
readonly isProtected: boolean;
135
}
136
137
interface UpstreamRef {
138
readonly remote: string;
139
readonly name: string;
140
readonly commit?: string;
141
}
142
143
interface GitRef {
144
type: GitRefType;
145
name?: string;
146
commit?: string;
147
remote?: string;
148
}
149
150
const enum GitRefType {
151
Head,
152
RemoteHead,
153
Tag
154
}
155
156
interface GitRefQuery {
157
readonly contains?: string;
158
readonly count?: number;
159
readonly pattern?: string | string[];
160
readonly sort?: 'alphabetically' | 'committerdate' | 'creatordate';
161
}
162
163
interface GitExtensionAPI {
164
openRepository(root: vscode.Uri): Promise<Repository | null>;
165
}
166
167
interface GitExtension {
168
getAPI(version: 1): GitExtensionAPI;
169
}
170
171
export interface IExtHostGitExtensionService extends ExtHostGitExtensionShape {
172
readonly _serviceBrand: undefined;
173
}
174
175
export const IExtHostGitExtensionService = createDecorator<IExtHostGitExtensionService>('IExtHostGitExtensionService');
176
177
export class ExtHostGitExtensionService extends Disposable implements IExtHostGitExtensionService {
178
declare readonly _serviceBrand: undefined;
179
180
private static _handlePool: number = 0;
181
182
private _gitApi: GitExtensionAPI | undefined;
183
184
private readonly _proxy: MainThreadGitExtensionShape;
185
186
private readonly _repositories = new Map<number, Repository>();
187
private readonly _repositoryByUri = new ResourceMap<number>();
188
private readonly _repositoryStateChangeListeners = new DisposableMap<number, vscode.Disposable>();
189
190
constructor(
191
@IExtHostRpcService extHostRpc: IExtHostRpcService,
192
@IExtHostExtensionService private readonly _extHostExtensionService: IExtHostExtensionService,
193
) {
194
super();
195
196
this._proxy = extHostRpc.getProxy(MainContext.MainThreadGitExtension);
197
}
198
199
async $isGitExtensionAvailable(): Promise<boolean> {
200
const registry = await this._extHostExtensionService.getExtensionRegistry();
201
return !!registry.getExtensionDescription(GIT_EXTENSION_ID);
202
}
203
204
async $openRepository(uri: UriComponents): Promise<{ handle: number; rootUri: UriComponents; state: GitRepositoryStateDto } | undefined> {
205
const api = await this._ensureGitApi();
206
if (!api) {
207
return undefined;
208
}
209
210
const repository = await api.openRepository(URI.revive(uri));
211
if (!repository) {
212
return undefined;
213
}
214
215
const existingHandle = this._repositoryByUri.get(repository.rootUri);
216
if (existingHandle !== undefined) {
217
if (this._repositories.get(existingHandle) !== repository) {
218
this._repositories.set(existingHandle, repository);
219
this._repositoryByUri.set(repository.rootUri, existingHandle);
220
221
this._setRepositoryStateChangeListener(existingHandle, repository);
222
}
223
224
const state = this._getRepositoryState(repository);
225
return { handle: existingHandle, rootUri: repository.rootUri, state };
226
}
227
228
// Store the repository and its handle in the maps
229
const handle = ExtHostGitExtensionService._handlePool++;
230
231
this._repositories.set(handle, repository);
232
this._repositoryByUri.set(repository.rootUri, handle);
233
234
this._setRepositoryStateChangeListener(handle, repository);
235
236
const state = this._getRepositoryState(repository);
237
return { handle, rootUri: repository.rootUri, state };
238
}
239
240
async $getRefs(handle: number, query: GitRefQueryDto, token?: vscode.CancellationToken): Promise<GitRefDto[]> {
241
const repository = this._repositories.get(handle);
242
if (!repository) {
243
return [];
244
}
245
246
try {
247
const refs = await repository.getRefs(query, token);
248
const result: (GitRefDto | undefined)[] = refs.map(ref => {
249
if (!ref.name || !ref.commit) {
250
return undefined;
251
}
252
253
const id = ref.type === GitRefType.Head
254
? `refs/heads/${ref.name}`
255
: ref.type === GitRefType.RemoteHead
256
? `refs/remotes/${ref.remote}/${ref.name}`
257
: `refs/tags/${ref.name}`;
258
259
return {
260
id,
261
name: ref.name,
262
type: toGitRefTypeDto(ref.type),
263
revision: ref.commit
264
} satisfies GitRefDto;
265
});
266
267
return result.filter(ref => !!ref);
268
} catch {
269
return [];
270
}
271
}
272
273
async $getRepositoryState(handle: number): Promise<GitRepositoryStateDto | undefined> {
274
const repository = this._repositories.get(handle);
275
if (!repository) {
276
return undefined;
277
}
278
279
return this._getRepositoryState(repository);
280
}
281
282
private _getRepositoryState(repository: Repository): GitRepositoryStateDto {
283
const state = repository.state;
284
285
return {
286
HEAD: state.HEAD ? toGitBranchDto(state.HEAD) : undefined,
287
remotes: state.remotes,
288
mergeChanges: state.mergeChanges.map(toGitChangeDto),
289
indexChanges: state.indexChanges.map(toGitChangeDto),
290
workingTreeChanges: state.workingTreeChanges.map(toGitChangeDto),
291
untrackedChanges: state.untrackedChanges.map(toGitChangeDto),
292
};
293
}
294
295
private _setRepositoryStateChangeListener(handle: number, repository: Repository): void {
296
this._repositoryStateChangeListeners.set(handle, repository.state.onDidChange(() => {
297
this._proxy.$onDidChangeRepository(handle);
298
}));
299
}
300
301
async $diffBetweenWithStats(handle: number, ref1: string, ref2: string, path?: string): Promise<GitDiffChangeDto[]> {
302
const repository = this._repositories.get(handle);
303
if (!repository) {
304
return [];
305
}
306
307
try {
308
const changes = await repository.diffBetweenWithStats(ref1, ref2, path);
309
return changes.map(c => ({
310
...toGitChangeDto(c),
311
insertions: c.insertions,
312
deletions: c.deletions,
313
}));
314
} catch {
315
return [];
316
}
317
}
318
319
async $diffBetweenWithStats2(handle: number, ref: string, path?: string): Promise<GitDiffChangeDto[]> {
320
const repository = this._repositories.get(handle);
321
if (!repository) {
322
return [];
323
}
324
325
try {
326
const changes = await repository.diffBetweenWithStats2(ref, path);
327
return changes.map(c => ({
328
...toGitChangeDto(c),
329
insertions: c.insertions,
330
deletions: c.deletions,
331
}));
332
} catch {
333
return [];
334
}
335
}
336
337
private async _ensureGitApi(): Promise<GitExtensionAPI | undefined> {
338
if (this._gitApi) {
339
return this._gitApi;
340
}
341
342
try {
343
await this._extHostExtensionService.activateByIdWithErrors(
344
new ExtensionIdentifier(GIT_EXTENSION_ID),
345
{ startup: false, extensionId: new ExtensionIdentifier(GIT_EXTENSION_ID), activationEvent: 'api' }
346
);
347
348
const exports = this._extHostExtensionService.getExtensionExports(new ExtensionIdentifier(GIT_EXTENSION_ID));
349
if (!!exports && typeof (exports as GitExtension).getAPI === 'function') {
350
this._gitApi = (exports as GitExtension).getAPI(1);
351
}
352
} catch {
353
// Git extension not available
354
}
355
356
return this._gitApi;
357
}
358
359
override dispose(): void {
360
this._repositoryStateChangeListeners.dispose();
361
super.dispose();
362
}
363
}
364
365