mirror of
https://github.com/microsoft/vscode.git
synced 2026-02-23 19:27:15 +00:00
197 lines
6.9 KiB
TypeScript
197 lines
6.9 KiB
TypeScript
/*---------------------------------------------------------------------------------------------
|
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
|
* Licensed under the MIT License. See License.txt in the project root for license information.
|
|
*--------------------------------------------------------------------------------------------*/
|
|
|
|
import { LogOutputChannel, SourceControlArtifactProvider, SourceControlArtifactGroup, SourceControlArtifact, Event, EventEmitter, ThemeIcon, l10n, workspace, Uri, Disposable, Command } from 'vscode';
|
|
import { coalesce, dispose, filterEvent, IDisposable, isCopilotWorktree } from './util';
|
|
import { Repository } from './repository';
|
|
import { Ref, RefType, Worktree } from './api/git';
|
|
import { OperationKind } from './operation';
|
|
|
|
/**
|
|
* Sorts refs like a directory tree: refs with more path segments (directories) appear first
|
|
* and are sorted alphabetically, while refs at the same level (files) maintain insertion order.
|
|
* Refs without '/' maintain their insertion order and appear after refs with '/'.
|
|
*/
|
|
function sortRefByName(refA: Ref, refB: Ref): number {
|
|
const nameA = refA.name ?? '';
|
|
const nameB = refB.name ?? '';
|
|
|
|
const lastSlashA = nameA.lastIndexOf('/');
|
|
const lastSlashB = nameB.lastIndexOf('/');
|
|
|
|
// Neither ref has a slash, maintain insertion order
|
|
if (lastSlashA === -1 && lastSlashB === -1) {
|
|
return 0;
|
|
}
|
|
|
|
// Ref with a slash comes first
|
|
if (lastSlashA !== -1 && lastSlashB === -1) {
|
|
return -1;
|
|
} else if (lastSlashA === -1 && lastSlashB !== -1) {
|
|
return 1;
|
|
}
|
|
|
|
// Both have slashes
|
|
// Get directory segments
|
|
const segmentsA = nameA.substring(0, lastSlashA).split('/');
|
|
const segmentsB = nameB.substring(0, lastSlashB).split('/');
|
|
|
|
// Compare directory segments
|
|
for (let index = 0; index < Math.min(segmentsA.length, segmentsB.length); index++) {
|
|
const result = segmentsA[index].localeCompare(segmentsB[index]);
|
|
if (result !== 0) {
|
|
return result;
|
|
}
|
|
}
|
|
|
|
// Directory with more segments comes first
|
|
if (segmentsA.length !== segmentsB.length) {
|
|
return segmentsB.length - segmentsA.length;
|
|
}
|
|
|
|
// Insertion order
|
|
return 0;
|
|
}
|
|
|
|
function sortByWorktreeTypeAndNameAsc(a: Worktree, b: Worktree): number {
|
|
if (a.main && !b.main) {
|
|
return -1;
|
|
} else if (!a.main && b.main) {
|
|
return 1;
|
|
} else {
|
|
return a.name.localeCompare(b.name);
|
|
}
|
|
}
|
|
|
|
export class GitArtifactProvider implements SourceControlArtifactProvider, IDisposable {
|
|
private readonly _onDidChangeArtifacts = new EventEmitter<string[]>();
|
|
readonly onDidChangeArtifacts: Event<string[]> = this._onDidChangeArtifacts.event;
|
|
|
|
private readonly _groups: SourceControlArtifactGroup[];
|
|
private readonly _disposables: Disposable[] = [];
|
|
|
|
constructor(
|
|
private readonly repository: Repository,
|
|
private readonly logger: LogOutputChannel
|
|
) {
|
|
this._groups = [
|
|
{ id: 'branches', name: l10n.t('Branches'), icon: new ThemeIcon('git-branch'), supportsFolders: true },
|
|
{ id: 'stashes', name: l10n.t('Stashes'), icon: new ThemeIcon('git-stash'), supportsFolders: false },
|
|
{ id: 'tags', name: l10n.t('Tags'), icon: new ThemeIcon('tag'), supportsFolders: true },
|
|
{ id: 'worktrees', name: l10n.t('Worktrees'), icon: new ThemeIcon('worktree'), supportsFolders: false }
|
|
];
|
|
|
|
this._disposables.push(this._onDidChangeArtifacts);
|
|
this._disposables.push(repository.historyProvider.onDidChangeHistoryItemRefs(e => {
|
|
const groups = new Set<string>();
|
|
for (const ref of e.added.concat(e.modified).concat(e.removed)) {
|
|
if (ref.id.startsWith('refs/heads/')) {
|
|
groups.add('branches');
|
|
} else if (ref.id.startsWith('refs/tags/')) {
|
|
groups.add('tags');
|
|
}
|
|
}
|
|
|
|
this._onDidChangeArtifacts.fire(Array.from(groups));
|
|
}));
|
|
|
|
const onDidRunWriteOperation = filterEvent(
|
|
repository.onDidRunOperation, e => !e.operation.readOnly);
|
|
|
|
this._disposables.push(onDidRunWriteOperation(result => {
|
|
if (result.operation.kind === OperationKind.Stash) {
|
|
this._onDidChangeArtifacts.fire(['stashes']);
|
|
} else if (result.operation.kind === OperationKind.Worktree) {
|
|
this._onDidChangeArtifacts.fire(['worktrees']);
|
|
}
|
|
}));
|
|
}
|
|
|
|
provideArtifactGroups(): SourceControlArtifactGroup[] {
|
|
return this._groups;
|
|
}
|
|
|
|
async provideArtifacts(group: string): Promise<SourceControlArtifact[]> {
|
|
const config = workspace.getConfiguration('git', Uri.file(this.repository.root));
|
|
const shortCommitLength = config.get<number>('commitShortHashLength', 7);
|
|
|
|
try {
|
|
if (group === 'branches') {
|
|
const refs = await this.repository
|
|
.getRefs({ pattern: 'refs/heads', includeCommitDetails: true, sort: 'creatordate' });
|
|
|
|
return refs.sort(sortRefByName).map(r => ({
|
|
id: `refs/heads/${r.name}`,
|
|
name: r.name ?? r.commit ?? '',
|
|
description: coalesce([
|
|
r.commit?.substring(0, shortCommitLength),
|
|
r.commitDetails?.message.split('\n')[0]
|
|
]).join(' \u2022 '),
|
|
icon: this.repository.HEAD?.type === RefType.Head && r.name === this.repository.HEAD?.name
|
|
? new ThemeIcon('target')
|
|
: new ThemeIcon('git-branch'),
|
|
timestamp: r.commitDetails?.commitDate?.getTime()
|
|
}));
|
|
} else if (group === 'tags') {
|
|
const refs = await this.repository
|
|
.getRefs({ pattern: 'refs/tags', includeCommitDetails: true, sort: 'creatordate' });
|
|
|
|
return refs.sort(sortRefByName).map(r => ({
|
|
id: `refs/tags/${r.name}`,
|
|
name: r.name ?? r.commit ?? '',
|
|
description: coalesce([
|
|
r.commit?.substring(0, shortCommitLength),
|
|
r.commitDetails?.message.split('\n')[0]
|
|
]).join(' \u2022 '),
|
|
icon: this.repository.HEAD?.type === RefType.Tag && r.name === this.repository.HEAD?.name
|
|
? new ThemeIcon('target')
|
|
: new ThemeIcon('tag'),
|
|
timestamp: r.commitDetails?.commitDate?.getTime()
|
|
}));
|
|
} else if (group === 'stashes') {
|
|
const stashes = await this.repository.getStashes();
|
|
|
|
return stashes.map(s => ({
|
|
id: `stash@{${s.index}}`,
|
|
name: s.description,
|
|
description: s.branchName,
|
|
icon: new ThemeIcon('git-stash'),
|
|
timestamp: s.commitDate?.getTime(),
|
|
command: {
|
|
title: l10n.t('View Stash'),
|
|
command: 'git.repositories.stashView'
|
|
} satisfies Command
|
|
}));
|
|
} else if (group === 'worktrees') {
|
|
const worktrees = await this.repository.getWorktreeDetails();
|
|
|
|
return worktrees.sort(sortByWorktreeTypeAndNameAsc).map(w => ({
|
|
id: w.path,
|
|
name: w.name,
|
|
description: coalesce([
|
|
w.detached ? l10n.t('detached') : w.ref.substring(11),
|
|
w.commitDetails?.hash.substring(0, shortCommitLength),
|
|
w.commitDetails?.message.split('\n')[0]
|
|
]).join(' \u2022 '),
|
|
icon: w.main
|
|
? new ThemeIcon('repo')
|
|
: isCopilotWorktree(w.path)
|
|
? new ThemeIcon('chat-sparkle')
|
|
: new ThemeIcon('worktree')
|
|
}));
|
|
}
|
|
} catch (err) {
|
|
this.logger.error(`[GitArtifactProvider][provideArtifacts] Error while providing artifacts for group '${group}': `, err);
|
|
return [];
|
|
}
|
|
|
|
return [];
|
|
}
|
|
|
|
dispose(): void {
|
|
dispose(this._disposables);
|
|
}
|
|
}
|