Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/git/src/artifactProvider.ts
5243 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 { LogOutputChannel, SourceControlArtifactProvider, SourceControlArtifactGroup, SourceControlArtifact, Event, EventEmitter, ThemeIcon, l10n, workspace, Uri, Disposable, Command } from 'vscode';
7
import { coalesce, dispose, filterEvent, IDisposable, isCopilotWorktree } from './util';
8
import { Repository } from './repository';
9
import { Ref, RefType, Worktree } from './api/git';
10
import { OperationKind } from './operation';
11
12
/**
13
* Sorts refs like a directory tree: refs with more path segments (directories) appear first
14
* and are sorted alphabetically, while refs at the same level (files) maintain insertion order.
15
* Refs without '/' maintain their insertion order and appear after refs with '/'.
16
*/
17
function sortRefByName(refA: Ref, refB: Ref): number {
18
const nameA = refA.name ?? '';
19
const nameB = refB.name ?? '';
20
21
const lastSlashA = nameA.lastIndexOf('/');
22
const lastSlashB = nameB.lastIndexOf('/');
23
24
// Neither ref has a slash, maintain insertion order
25
if (lastSlashA === -1 && lastSlashB === -1) {
26
return 0;
27
}
28
29
// Ref with a slash comes first
30
if (lastSlashA !== -1 && lastSlashB === -1) {
31
return -1;
32
} else if (lastSlashA === -1 && lastSlashB !== -1) {
33
return 1;
34
}
35
36
// Both have slashes
37
// Get directory segments
38
const segmentsA = nameA.substring(0, lastSlashA).split('/');
39
const segmentsB = nameB.substring(0, lastSlashB).split('/');
40
41
// Compare directory segments
42
for (let index = 0; index < Math.min(segmentsA.length, segmentsB.length); index++) {
43
const result = segmentsA[index].localeCompare(segmentsB[index]);
44
if (result !== 0) {
45
return result;
46
}
47
}
48
49
// Directory with more segments comes first
50
if (segmentsA.length !== segmentsB.length) {
51
return segmentsB.length - segmentsA.length;
52
}
53
54
// Insertion order
55
return 0;
56
}
57
58
function sortByWorktreeTypeAndNameAsc(a: Worktree, b: Worktree): number {
59
if (a.main && !b.main) {
60
return -1;
61
} else if (!a.main && b.main) {
62
return 1;
63
} else {
64
return a.name.localeCompare(b.name);
65
}
66
}
67
68
export class GitArtifactProvider implements SourceControlArtifactProvider, IDisposable {
69
private readonly _onDidChangeArtifacts = new EventEmitter<string[]>();
70
readonly onDidChangeArtifacts: Event<string[]> = this._onDidChangeArtifacts.event;
71
72
private readonly _groups: SourceControlArtifactGroup[];
73
private readonly _disposables: Disposable[] = [];
74
75
constructor(
76
private readonly repository: Repository,
77
private readonly logger: LogOutputChannel
78
) {
79
this._groups = [
80
{ id: 'branches', name: l10n.t('Branches'), icon: new ThemeIcon('git-branch'), supportsFolders: true },
81
{ id: 'stashes', name: l10n.t('Stashes'), icon: new ThemeIcon('git-stash'), supportsFolders: false },
82
{ id: 'tags', name: l10n.t('Tags'), icon: new ThemeIcon('tag'), supportsFolders: true },
83
{ id: 'worktrees', name: l10n.t('Worktrees'), icon: new ThemeIcon('worktree'), supportsFolders: false }
84
];
85
86
this._disposables.push(this._onDidChangeArtifacts);
87
this._disposables.push(repository.historyProvider.onDidChangeHistoryItemRefs(e => {
88
const groups = new Set<string>();
89
for (const ref of e.added.concat(e.modified).concat(e.removed)) {
90
if (ref.id.startsWith('refs/heads/')) {
91
groups.add('branches');
92
} else if (ref.id.startsWith('refs/tags/')) {
93
groups.add('tags');
94
}
95
}
96
97
this._onDidChangeArtifacts.fire(Array.from(groups));
98
}));
99
100
const onDidRunWriteOperation = filterEvent(
101
repository.onDidRunOperation, e => !e.operation.readOnly);
102
103
this._disposables.push(onDidRunWriteOperation(result => {
104
if (result.operation.kind === OperationKind.Stash) {
105
this._onDidChangeArtifacts.fire(['stashes']);
106
} else if (result.operation.kind === OperationKind.Worktree) {
107
this._onDidChangeArtifacts.fire(['worktrees']);
108
}
109
}));
110
}
111
112
provideArtifactGroups(): SourceControlArtifactGroup[] {
113
return this._groups;
114
}
115
116
async provideArtifacts(group: string): Promise<SourceControlArtifact[]> {
117
const config = workspace.getConfiguration('git', Uri.file(this.repository.root));
118
const shortCommitLength = config.get<number>('commitShortHashLength', 7);
119
120
try {
121
if (group === 'branches') {
122
const refs = await this.repository
123
.getRefs({ pattern: 'refs/heads', includeCommitDetails: true, sort: 'creatordate' });
124
125
return refs.sort(sortRefByName).map(r => ({
126
id: `refs/heads/${r.name}`,
127
name: r.name ?? r.commit ?? '',
128
description: coalesce([
129
r.commit?.substring(0, shortCommitLength),
130
r.commitDetails?.message.split('\n')[0]
131
]).join(' \u2022 '),
132
icon: this.repository.HEAD?.type === RefType.Head && r.name === this.repository.HEAD?.name
133
? new ThemeIcon('target')
134
: new ThemeIcon('git-branch'),
135
timestamp: r.commitDetails?.commitDate?.getTime()
136
}));
137
} else if (group === 'tags') {
138
const refs = await this.repository
139
.getRefs({ pattern: 'refs/tags', includeCommitDetails: true, sort: 'creatordate' });
140
141
return refs.sort(sortRefByName).map(r => ({
142
id: `refs/tags/${r.name}`,
143
name: r.name ?? r.commit ?? '',
144
description: coalesce([
145
r.commit?.substring(0, shortCommitLength),
146
r.commitDetails?.message.split('\n')[0]
147
]).join(' \u2022 '),
148
icon: this.repository.HEAD?.type === RefType.Tag && r.name === this.repository.HEAD?.name
149
? new ThemeIcon('target')
150
: new ThemeIcon('tag'),
151
timestamp: r.commitDetails?.commitDate?.getTime()
152
}));
153
} else if (group === 'stashes') {
154
const stashes = await this.repository.getStashes();
155
156
return stashes.map(s => ({
157
id: `stash@{${s.index}}`,
158
name: s.description,
159
description: s.branchName,
160
icon: new ThemeIcon('git-stash'),
161
timestamp: s.commitDate?.getTime(),
162
command: {
163
title: l10n.t('View Stash'),
164
command: 'git.repositories.stashView'
165
} satisfies Command
166
}));
167
} else if (group === 'worktrees') {
168
const worktrees = await this.repository.getWorktreeDetails();
169
170
return worktrees.sort(sortByWorktreeTypeAndNameAsc).map(w => ({
171
id: w.path,
172
name: w.name,
173
description: coalesce([
174
w.detached ? l10n.t('detached') : w.ref.substring(11),
175
w.commitDetails?.hash.substring(0, shortCommitLength),
176
w.commitDetails?.message.split('\n')[0]
177
]).join(' \u2022 '),
178
icon: w.main
179
? new ThemeIcon('repo')
180
: isCopilotWorktree(w.path)
181
? new ThemeIcon('chat-sparkle')
182
: new ThemeIcon('worktree')
183
}));
184
}
185
} catch (err) {
186
this.logger.error(`[GitArtifactProvider][provideArtifacts] Error while providing artifacts for group '${group}': `, err);
187
return [];
188
}
189
190
return [];
191
}
192
193
dispose(): void {
194
dispose(this._disposables);
195
}
196
}
197
198