Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/git/src/artifactProvider.ts
4772 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 { Commit, Ref, RefType } 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 sortByCommitDateDesc(a: { commitDetails?: Commit }, b: { commitDetails?: Commit }): number {
59
const aCommitDate = a.commitDetails?.commitDate?.getTime() ?? 0;
60
const bCommitDate = b.commitDetails?.commitDate?.getTime() ?? 0;
61
62
return bCommitDate - aCommitDate;
63
}
64
65
export class GitArtifactProvider implements SourceControlArtifactProvider, IDisposable {
66
private readonly _onDidChangeArtifacts = new EventEmitter<string[]>();
67
readonly onDidChangeArtifacts: Event<string[]> = this._onDidChangeArtifacts.event;
68
69
private readonly _groups: SourceControlArtifactGroup[];
70
private readonly _disposables: Disposable[] = [];
71
72
constructor(
73
private readonly repository: Repository,
74
private readonly logger: LogOutputChannel
75
) {
76
this._groups = [
77
{ id: 'branches', name: l10n.t('Branches'), icon: new ThemeIcon('git-branch'), supportsFolders: true },
78
{ id: 'stashes', name: l10n.t('Stashes'), icon: new ThemeIcon('git-stash'), supportsFolders: false },
79
{ id: 'tags', name: l10n.t('Tags'), icon: new ThemeIcon('tag'), supportsFolders: true },
80
{ id: 'worktrees', name: l10n.t('Worktrees'), icon: new ThemeIcon('worktree'), supportsFolders: false }
81
];
82
83
this._disposables.push(this._onDidChangeArtifacts);
84
this._disposables.push(repository.historyProvider.onDidChangeHistoryItemRefs(e => {
85
const groups = new Set<string>();
86
for (const ref of e.added.concat(e.modified).concat(e.removed)) {
87
if (ref.id.startsWith('refs/heads/')) {
88
groups.add('branches');
89
} else if (ref.id.startsWith('refs/tags/')) {
90
groups.add('tags');
91
}
92
}
93
94
this._onDidChangeArtifacts.fire(Array.from(groups));
95
}));
96
97
const onDidRunWriteOperation = filterEvent(
98
repository.onDidRunOperation, e => !e.operation.readOnly);
99
100
this._disposables.push(onDidRunWriteOperation(result => {
101
if (result.operation.kind === OperationKind.Stash) {
102
this._onDidChangeArtifacts.fire(['stashes']);
103
} else if (result.operation.kind === OperationKind.Worktree) {
104
this._onDidChangeArtifacts.fire(['worktrees']);
105
}
106
}));
107
}
108
109
provideArtifactGroups(): SourceControlArtifactGroup[] {
110
return this._groups;
111
}
112
113
async provideArtifacts(group: string): Promise<SourceControlArtifact[]> {
114
const config = workspace.getConfiguration('git', Uri.file(this.repository.root));
115
const shortCommitLength = config.get<number>('commitShortHashLength', 7);
116
117
try {
118
if (group === 'branches') {
119
const refs = await this.repository
120
.getRefs({ pattern: 'refs/heads', includeCommitDetails: true, sort: 'creatordate' });
121
122
return refs.sort(sortRefByName).map(r => ({
123
id: `refs/heads/${r.name}`,
124
name: r.name ?? r.commit ?? '',
125
description: coalesce([
126
r.commit?.substring(0, shortCommitLength),
127
r.commitDetails?.message.split('\n')[0]
128
]).join(' \u2022 '),
129
icon: this.repository.HEAD?.type === RefType.Head && r.name === this.repository.HEAD?.name
130
? new ThemeIcon('target')
131
: new ThemeIcon('git-branch'),
132
timestamp: r.commitDetails?.commitDate?.getTime()
133
}));
134
} else if (group === 'tags') {
135
const refs = await this.repository
136
.getRefs({ pattern: 'refs/tags', includeCommitDetails: true, sort: 'creatordate' });
137
138
return refs.sort(sortRefByName).map(r => ({
139
id: `refs/tags/${r.name}`,
140
name: r.name ?? r.commit ?? '',
141
description: coalesce([
142
r.commit?.substring(0, shortCommitLength),
143
r.commitDetails?.message.split('\n')[0]
144
]).join(' \u2022 '),
145
icon: this.repository.HEAD?.type === RefType.Tag && r.name === this.repository.HEAD?.name
146
? new ThemeIcon('target')
147
: new ThemeIcon('tag'),
148
timestamp: r.commitDetails?.commitDate?.getTime()
149
}));
150
} else if (group === 'stashes') {
151
const stashes = await this.repository.getStashes();
152
153
return stashes.map(s => ({
154
id: `stash@{${s.index}}`,
155
name: s.description,
156
description: s.branchName,
157
icon: new ThemeIcon('git-stash'),
158
timestamp: s.commitDate?.getTime(),
159
command: {
160
title: l10n.t('View Stash'),
161
command: 'git.repositories.stashView'
162
} satisfies Command
163
}));
164
} else if (group === 'worktrees') {
165
const worktrees = await this.repository.getWorktreeDetails();
166
167
return worktrees.sort(sortByCommitDateDesc).map(w => ({
168
id: w.path,
169
name: w.name,
170
description: coalesce([
171
w.detached ? l10n.t('detached') : w.ref.substring(11),
172
w.commitDetails?.hash.substring(0, shortCommitLength),
173
w.commitDetails?.message.split('\n')[0]
174
]).join(' \u2022 '),
175
icon: isCopilotWorktree(w.path)
176
? new ThemeIcon('chat-sparkle')
177
: new ThemeIcon('worktree'),
178
timestamp: w.commitDetails?.commitDate?.getTime(),
179
}));
180
}
181
} catch (err) {
182
this.logger.error(`[GitArtifactProvider][provideArtifacts] Error while providing artifacts for group '${group}': `, err);
183
return [];
184
}
185
186
return [];
187
}
188
189
dispose(): void {
190
dispose(this._disposables);
191
}
192
}
193
194