Path: blob/main/extensions/git/src/artifactProvider.ts
5243 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 { LogOutputChannel, SourceControlArtifactProvider, SourceControlArtifactGroup, SourceControlArtifact, Event, EventEmitter, ThemeIcon, l10n, workspace, Uri, Disposable, Command } from 'vscode';6import { coalesce, dispose, filterEvent, IDisposable, isCopilotWorktree } from './util';7import { Repository } from './repository';8import { Ref, RefType, Worktree } from './api/git';9import { OperationKind } from './operation';1011/**12* Sorts refs like a directory tree: refs with more path segments (directories) appear first13* and are sorted alphabetically, while refs at the same level (files) maintain insertion order.14* Refs without '/' maintain their insertion order and appear after refs with '/'.15*/16function sortRefByName(refA: Ref, refB: Ref): number {17const nameA = refA.name ?? '';18const nameB = refB.name ?? '';1920const lastSlashA = nameA.lastIndexOf('/');21const lastSlashB = nameB.lastIndexOf('/');2223// Neither ref has a slash, maintain insertion order24if (lastSlashA === -1 && lastSlashB === -1) {25return 0;26}2728// Ref with a slash comes first29if (lastSlashA !== -1 && lastSlashB === -1) {30return -1;31} else if (lastSlashA === -1 && lastSlashB !== -1) {32return 1;33}3435// Both have slashes36// Get directory segments37const segmentsA = nameA.substring(0, lastSlashA).split('/');38const segmentsB = nameB.substring(0, lastSlashB).split('/');3940// Compare directory segments41for (let index = 0; index < Math.min(segmentsA.length, segmentsB.length); index++) {42const result = segmentsA[index].localeCompare(segmentsB[index]);43if (result !== 0) {44return result;45}46}4748// Directory with more segments comes first49if (segmentsA.length !== segmentsB.length) {50return segmentsB.length - segmentsA.length;51}5253// Insertion order54return 0;55}5657function sortByWorktreeTypeAndNameAsc(a: Worktree, b: Worktree): number {58if (a.main && !b.main) {59return -1;60} else if (!a.main && b.main) {61return 1;62} else {63return a.name.localeCompare(b.name);64}65}6667export class GitArtifactProvider implements SourceControlArtifactProvider, IDisposable {68private readonly _onDidChangeArtifacts = new EventEmitter<string[]>();69readonly onDidChangeArtifacts: Event<string[]> = this._onDidChangeArtifacts.event;7071private readonly _groups: SourceControlArtifactGroup[];72private readonly _disposables: Disposable[] = [];7374constructor(75private readonly repository: Repository,76private readonly logger: LogOutputChannel77) {78this._groups = [79{ id: 'branches', name: l10n.t('Branches'), icon: new ThemeIcon('git-branch'), supportsFolders: true },80{ id: 'stashes', name: l10n.t('Stashes'), icon: new ThemeIcon('git-stash'), supportsFolders: false },81{ id: 'tags', name: l10n.t('Tags'), icon: new ThemeIcon('tag'), supportsFolders: true },82{ id: 'worktrees', name: l10n.t('Worktrees'), icon: new ThemeIcon('worktree'), supportsFolders: false }83];8485this._disposables.push(this._onDidChangeArtifacts);86this._disposables.push(repository.historyProvider.onDidChangeHistoryItemRefs(e => {87const groups = new Set<string>();88for (const ref of e.added.concat(e.modified).concat(e.removed)) {89if (ref.id.startsWith('refs/heads/')) {90groups.add('branches');91} else if (ref.id.startsWith('refs/tags/')) {92groups.add('tags');93}94}9596this._onDidChangeArtifacts.fire(Array.from(groups));97}));9899const onDidRunWriteOperation = filterEvent(100repository.onDidRunOperation, e => !e.operation.readOnly);101102this._disposables.push(onDidRunWriteOperation(result => {103if (result.operation.kind === OperationKind.Stash) {104this._onDidChangeArtifacts.fire(['stashes']);105} else if (result.operation.kind === OperationKind.Worktree) {106this._onDidChangeArtifacts.fire(['worktrees']);107}108}));109}110111provideArtifactGroups(): SourceControlArtifactGroup[] {112return this._groups;113}114115async provideArtifacts(group: string): Promise<SourceControlArtifact[]> {116const config = workspace.getConfiguration('git', Uri.file(this.repository.root));117const shortCommitLength = config.get<number>('commitShortHashLength', 7);118119try {120if (group === 'branches') {121const refs = await this.repository122.getRefs({ pattern: 'refs/heads', includeCommitDetails: true, sort: 'creatordate' });123124return refs.sort(sortRefByName).map(r => ({125id: `refs/heads/${r.name}`,126name: r.name ?? r.commit ?? '',127description: coalesce([128r.commit?.substring(0, shortCommitLength),129r.commitDetails?.message.split('\n')[0]130]).join(' \u2022 '),131icon: this.repository.HEAD?.type === RefType.Head && r.name === this.repository.HEAD?.name132? new ThemeIcon('target')133: new ThemeIcon('git-branch'),134timestamp: r.commitDetails?.commitDate?.getTime()135}));136} else if (group === 'tags') {137const refs = await this.repository138.getRefs({ pattern: 'refs/tags', includeCommitDetails: true, sort: 'creatordate' });139140return refs.sort(sortRefByName).map(r => ({141id: `refs/tags/${r.name}`,142name: r.name ?? r.commit ?? '',143description: coalesce([144r.commit?.substring(0, shortCommitLength),145r.commitDetails?.message.split('\n')[0]146]).join(' \u2022 '),147icon: this.repository.HEAD?.type === RefType.Tag && r.name === this.repository.HEAD?.name148? new ThemeIcon('target')149: new ThemeIcon('tag'),150timestamp: r.commitDetails?.commitDate?.getTime()151}));152} else if (group === 'stashes') {153const stashes = await this.repository.getStashes();154155return stashes.map(s => ({156id: `stash@{${s.index}}`,157name: s.description,158description: s.branchName,159icon: new ThemeIcon('git-stash'),160timestamp: s.commitDate?.getTime(),161command: {162title: l10n.t('View Stash'),163command: 'git.repositories.stashView'164} satisfies Command165}));166} else if (group === 'worktrees') {167const worktrees = await this.repository.getWorktreeDetails();168169return worktrees.sort(sortByWorktreeTypeAndNameAsc).map(w => ({170id: w.path,171name: w.name,172description: coalesce([173w.detached ? l10n.t('detached') : w.ref.substring(11),174w.commitDetails?.hash.substring(0, shortCommitLength),175w.commitDetails?.message.split('\n')[0]176]).join(' \u2022 '),177icon: w.main178? new ThemeIcon('repo')179: isCopilotWorktree(w.path)180? new ThemeIcon('chat-sparkle')181: new ThemeIcon('worktree')182}));183}184} catch (err) {185this.logger.error(`[GitArtifactProvider][provideArtifacts] Error while providing artifacts for group '${group}': `, err);186return [];187}188189return [];190}191192dispose(): void {193dispose(this._disposables);194}195}196197198