Path: blob/main/src/vs/workbench/api/common/extHostGitExtensionService.ts
13397 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import type * as vscode from 'vscode';6import { Event } from '../../../base/common/event.js';7import { Disposable, DisposableMap } from '../../../base/common/lifecycle.js';8import { URI, UriComponents } from '../../../base/common/uri.js';9import { ExtensionIdentifier } from '../../../platform/extensions/common/extensions.js';10import { createDecorator } from '../../../platform/instantiation/common/instantiation.js';11import { IExtHostExtensionService } from './extHostExtensionService.js';12import { IExtHostRpcService } from './extHostRpcService.js';13import { ExtHostGitExtensionShape, GitBranchDto, GitChangeDto, GitDiffChangeDto, GitRefDto, GitRefQueryDto, GitRefTypeDto, GitRepositoryStateDto, GitUpstreamRefDto, MainContext, MainThreadGitExtensionShape } from './extHost.protocol.js';14import { ResourceMap } from '../../../base/common/map.js';1516const GIT_EXTENSION_ID = 'vscode.git';1718function toGitRefTypeDto(type: GitRefType): GitRefTypeDto {19switch (type) {20case GitRefType.Head: return GitRefTypeDto.Head;21case GitRefType.RemoteHead: return GitRefTypeDto.RemoteHead;22case GitRefType.Tag: return GitRefTypeDto.Tag;23default: throw new Error(`Unknown GitRefType: ${type}`);24}25}2627function toGitBranchDto(branch: Branch): GitBranchDto {28return {29name: branch.name,30commit: branch.commit,31type: toGitRefTypeDto(branch.type),32remote: branch.remote,33upstream: branch.upstream ? toGitUpstreamRefDto(branch.upstream) : undefined,34ahead: branch.ahead,35behind: branch.behind,36};37}3839function toGitUpstreamRefDto(upstream: UpstreamRef): GitUpstreamRefDto {40return {41remote: upstream.remote,42name: upstream.name,43commit: upstream.commit,44};45}4647// Status values from the git extension's const enum Status48const enum GitStatus {49INDEX_ADDED = 1,50INDEX_DELETED = 2,51INDEX_RENAMED = 3,52MODIFIED = 5,53DELETED = 6,54UNTRACKED = 7,55INTENT_TO_ADD = 9,56INTENT_TO_RENAME = 10,57}5859function toGitChangeDto(change: Change): GitChangeDto {60switch (change.status) {61// Added: no original62case GitStatus.INDEX_ADDED:63case GitStatus.UNTRACKED:64case GitStatus.INTENT_TO_ADD:65return { uri: change.uri, originalUri: undefined, modifiedUri: change.uri };6667// Deleted: no modified68case GitStatus.INDEX_DELETED:69case GitStatus.DELETED:70return { uri: change.uri, originalUri: change.uri, modifiedUri: undefined };7172// Renamed: original is old name, modified is new name73case GitStatus.INDEX_RENAMED:74case GitStatus.INTENT_TO_RENAME:75return { uri: change.uri, originalUri: change.originalUri, modifiedUri: change.renameUri };7677// Modified and everything else: both original and modified78default:79return { uri: change.uri, originalUri: change.originalUri, modifiedUri: change.uri };80}81}8283interface DiffChange extends Change {84readonly insertions: number;85readonly deletions: number;86}8788interface Repository {89readonly rootUri: vscode.Uri;90readonly state: RepositoryState;9192status(): Promise<void>;93getBranchBase(name: string): Promise<Branch | undefined>;94getRefs(query: GitRefQuery, token?: vscode.CancellationToken): Promise<GitRef[]>;95diffBetweenWithStats(ref1: string, ref2: string, path?: string): Promise<DiffChange[]>;96diffBetweenWithStats2(ref: string, path?: string): Promise<DiffChange[]>;97isBranchProtected(branch?: Branch): boolean;98}99100interface Change {101readonly uri: vscode.Uri;102readonly originalUri: vscode.Uri;103readonly renameUri: vscode.Uri | undefined;104readonly status: number;105}106107interface RepositoryState {108readonly HEAD: Branch | undefined;109readonly remotes: Remote[];110readonly mergeChanges: Change[];111readonly indexChanges: Change[];112readonly workingTreeChanges: Change[];113readonly untrackedChanges: Change[];114readonly onDidChange: Event<void>;115}116117interface Remote {118readonly name: string;119readonly fetchUrl?: string;120readonly pushUrl?: string;121readonly isReadOnly: boolean;122}123124interface Branch extends GitRef {125readonly base?: BaseRef;126readonly upstream?: UpstreamRef;127readonly ahead?: number;128readonly behind?: number;129}130131interface BaseRef {132readonly name: string;133readonly isProtected: boolean;134}135136interface UpstreamRef {137readonly remote: string;138readonly name: string;139readonly commit?: string;140}141142interface GitRef {143type: GitRefType;144name?: string;145commit?: string;146remote?: string;147}148149const enum GitRefType {150Head,151RemoteHead,152Tag153}154155interface GitRefQuery {156readonly contains?: string;157readonly count?: number;158readonly pattern?: string | string[];159readonly sort?: 'alphabetically' | 'committerdate' | 'creatordate';160}161162interface GitExtensionAPI {163openRepository(root: vscode.Uri): Promise<Repository | null>;164}165166interface GitExtension {167getAPI(version: 1): GitExtensionAPI;168}169170export interface IExtHostGitExtensionService extends ExtHostGitExtensionShape {171readonly _serviceBrand: undefined;172}173174export const IExtHostGitExtensionService = createDecorator<IExtHostGitExtensionService>('IExtHostGitExtensionService');175176export class ExtHostGitExtensionService extends Disposable implements IExtHostGitExtensionService {177declare readonly _serviceBrand: undefined;178179private static _handlePool: number = 0;180181private _gitApi: GitExtensionAPI | undefined;182183private readonly _proxy: MainThreadGitExtensionShape;184185private readonly _repositories = new Map<number, Repository>();186private readonly _repositoryByUri = new ResourceMap<number>();187private readonly _repositoryStateChangeListeners = new DisposableMap<number, vscode.Disposable>();188189constructor(190@IExtHostRpcService extHostRpc: IExtHostRpcService,191@IExtHostExtensionService private readonly _extHostExtensionService: IExtHostExtensionService,192) {193super();194195this._proxy = extHostRpc.getProxy(MainContext.MainThreadGitExtension);196}197198async $isGitExtensionAvailable(): Promise<boolean> {199const registry = await this._extHostExtensionService.getExtensionRegistry();200return !!registry.getExtensionDescription(GIT_EXTENSION_ID);201}202203async $openRepository(uri: UriComponents): Promise<{ handle: number; rootUri: UriComponents; state: GitRepositoryStateDto } | undefined> {204const api = await this._ensureGitApi();205if (!api) {206return undefined;207}208209const repository = await api.openRepository(URI.revive(uri));210if (!repository) {211return undefined;212}213214const existingHandle = this._repositoryByUri.get(repository.rootUri);215if (existingHandle !== undefined) {216if (this._repositories.get(existingHandle) !== repository) {217this._repositories.set(existingHandle, repository);218this._repositoryByUri.set(repository.rootUri, existingHandle);219220this._setRepositoryStateChangeListener(existingHandle, repository);221}222223const state = this._getRepositoryState(repository);224return { handle: existingHandle, rootUri: repository.rootUri, state };225}226227// Store the repository and its handle in the maps228const handle = ExtHostGitExtensionService._handlePool++;229230this._repositories.set(handle, repository);231this._repositoryByUri.set(repository.rootUri, handle);232233this._setRepositoryStateChangeListener(handle, repository);234235const state = this._getRepositoryState(repository);236return { handle, rootUri: repository.rootUri, state };237}238239async $getRefs(handle: number, query: GitRefQueryDto, token?: vscode.CancellationToken): Promise<GitRefDto[]> {240const repository = this._repositories.get(handle);241if (!repository) {242return [];243}244245try {246const refs = await repository.getRefs(query, token);247const result: (GitRefDto | undefined)[] = refs.map(ref => {248if (!ref.name || !ref.commit) {249return undefined;250}251252const id = ref.type === GitRefType.Head253? `refs/heads/${ref.name}`254: ref.type === GitRefType.RemoteHead255? `refs/remotes/${ref.remote}/${ref.name}`256: `refs/tags/${ref.name}`;257258return {259id,260name: ref.name,261type: toGitRefTypeDto(ref.type),262revision: ref.commit263} satisfies GitRefDto;264});265266return result.filter(ref => !!ref);267} catch {268return [];269}270}271272async $getRepositoryState(handle: number): Promise<GitRepositoryStateDto | undefined> {273const repository = this._repositories.get(handle);274if (!repository) {275return undefined;276}277278return this._getRepositoryState(repository);279}280281private _getRepositoryState(repository: Repository): GitRepositoryStateDto {282const state = repository.state;283284return {285HEAD: state.HEAD ? toGitBranchDto(state.HEAD) : undefined,286remotes: state.remotes,287mergeChanges: state.mergeChanges.map(toGitChangeDto),288indexChanges: state.indexChanges.map(toGitChangeDto),289workingTreeChanges: state.workingTreeChanges.map(toGitChangeDto),290untrackedChanges: state.untrackedChanges.map(toGitChangeDto),291};292}293294private _setRepositoryStateChangeListener(handle: number, repository: Repository): void {295this._repositoryStateChangeListeners.set(handle, repository.state.onDidChange(() => {296this._proxy.$onDidChangeRepository(handle);297}));298}299300async $diffBetweenWithStats(handle: number, ref1: string, ref2: string, path?: string): Promise<GitDiffChangeDto[]> {301const repository = this._repositories.get(handle);302if (!repository) {303return [];304}305306try {307const changes = await repository.diffBetweenWithStats(ref1, ref2, path);308return changes.map(c => ({309...toGitChangeDto(c),310insertions: c.insertions,311deletions: c.deletions,312}));313} catch {314return [];315}316}317318async $diffBetweenWithStats2(handle: number, ref: string, path?: string): Promise<GitDiffChangeDto[]> {319const repository = this._repositories.get(handle);320if (!repository) {321return [];322}323324try {325const changes = await repository.diffBetweenWithStats2(ref, path);326return changes.map(c => ({327...toGitChangeDto(c),328insertions: c.insertions,329deletions: c.deletions,330}));331} catch {332return [];333}334}335336private async _ensureGitApi(): Promise<GitExtensionAPI | undefined> {337if (this._gitApi) {338return this._gitApi;339}340341try {342await this._extHostExtensionService.activateByIdWithErrors(343new ExtensionIdentifier(GIT_EXTENSION_ID),344{ startup: false, extensionId: new ExtensionIdentifier(GIT_EXTENSION_ID), activationEvent: 'api' }345);346347const exports = this._extHostExtensionService.getExtensionExports(new ExtensionIdentifier(GIT_EXTENSION_ID));348if (!!exports && typeof (exports as GitExtension).getAPI === 'function') {349this._gitApi = (exports as GitExtension).getAPI(1);350}351} catch {352// Git extension not available353}354355return this._gitApi;356}357358override dispose(): void {359this._repositoryStateChangeListeners.dispose();360super.dispose();361}362}363364365