Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/git/src/historyProvider.ts
5221 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
7
import { CancellationToken, Disposable, Event, EventEmitter, FileDecoration, FileDecorationProvider, SourceControlHistoryItem, SourceControlHistoryItemChange, SourceControlHistoryOptions, SourceControlHistoryProvider, ThemeIcon, Uri, window, LogOutputChannel, SourceControlHistoryItemRef, l10n, SourceControlHistoryItemRefsChangeEvent, workspace, ConfigurationChangeEvent, Command, commands } from 'vscode';
8
import { Repository, Resource } from './repository';
9
import { IDisposable, deltaHistoryItemRefs, dispose, filterEvent, subject, truncate } from './util';
10
import { toMultiFileDiffEditorUris } from './uri';
11
import { AvatarQuery, AvatarQueryCommit, Branch, LogOptions, Ref, RefType } from './api/git';
12
import { emojify, ensureEmojis } from './emoji';
13
import { Commit } from './git';
14
import { OperationKind, OperationResult } from './operation';
15
import { ISourceControlHistoryItemDetailsProviderRegistry, provideSourceControlHistoryItemAvatar, provideSourceControlHistoryItemHoverCommands, provideSourceControlHistoryItemMessageLinks } from './historyItemDetailsProvider';
16
import { throttle } from './decorators';
17
import { getHistoryItemHover, getHoverCommitHashCommands, processHoverRemoteCommands } from './hover';
18
19
function compareSourceControlHistoryItemRef(ref1: SourceControlHistoryItemRef, ref2: SourceControlHistoryItemRef): number {
20
const getOrder = (ref: SourceControlHistoryItemRef): number => {
21
if (ref.id.startsWith('refs/heads/')) {
22
return 1;
23
} else if (ref.id.startsWith('refs/remotes/')) {
24
return 2;
25
} else if (ref.id.startsWith('refs/tags/')) {
26
return 3;
27
}
28
29
return 99;
30
};
31
32
const ref1Order = getOrder(ref1);
33
const ref2Order = getOrder(ref2);
34
35
if (ref1Order !== ref2Order) {
36
return ref1Order - ref2Order;
37
}
38
39
return ref1.name.localeCompare(ref2.name);
40
}
41
42
export class GitHistoryProvider implements SourceControlHistoryProvider, FileDecorationProvider, IDisposable {
43
private readonly _onDidChangeDecorations = new EventEmitter<Uri[]>();
44
readonly onDidChangeFileDecorations: Event<Uri[]> = this._onDidChangeDecorations.event;
45
46
private _currentHistoryItemRef: SourceControlHistoryItemRef | undefined;
47
get currentHistoryItemRef(): SourceControlHistoryItemRef | undefined { return this._currentHistoryItemRef; }
48
49
private _currentHistoryItemRemoteRef: SourceControlHistoryItemRef | undefined;
50
get currentHistoryItemRemoteRef(): SourceControlHistoryItemRef | undefined { return this._currentHistoryItemRemoteRef; }
51
52
private _currentHistoryItemBaseRef: SourceControlHistoryItemRef | undefined;
53
get currentHistoryItemBaseRef(): SourceControlHistoryItemRef | undefined { return this._currentHistoryItemBaseRef; }
54
55
private readonly _onDidChangeCurrentHistoryItemRefs = new EventEmitter<void>();
56
readonly onDidChangeCurrentHistoryItemRefs: Event<void> = this._onDidChangeCurrentHistoryItemRefs.event;
57
58
private readonly _onDidChangeHistoryItemRefs = new EventEmitter<SourceControlHistoryItemRefsChangeEvent>();
59
readonly onDidChangeHistoryItemRefs: Event<SourceControlHistoryItemRefsChangeEvent> = this._onDidChangeHistoryItemRefs.event;
60
61
private _HEAD: Branch | undefined;
62
private _historyItemRefs: SourceControlHistoryItemRef[] = [];
63
64
private commitShortHashLength = 7;
65
private historyItemDecorations = new Map<string, FileDecoration>();
66
67
private disposables: Disposable[] = [];
68
69
constructor(
70
private historyItemDetailProviderRegistry: ISourceControlHistoryItemDetailsProviderRegistry,
71
private readonly repository: Repository,
72
private readonly logger: LogOutputChannel
73
) {
74
this.disposables.push(workspace.onDidChangeConfiguration(this.onDidChangeConfiguration));
75
this.onDidChangeConfiguration();
76
77
const onDidRunWriteOperation = filterEvent(repository.onDidRunOperation, e => !e.operation.readOnly);
78
this.disposables.push(onDidRunWriteOperation(this.onDidRunWriteOperation, this));
79
80
this.disposables.push(window.registerFileDecorationProvider(this));
81
}
82
83
private onDidChangeConfiguration(e?: ConfigurationChangeEvent): void {
84
if (e && !e.affectsConfiguration('git.commitShortHashLength')) {
85
return;
86
}
87
88
const config = workspace.getConfiguration('git', Uri.file(this.repository.root));
89
this.commitShortHashLength = config.get<number>('commitShortHashLength', 7);
90
}
91
92
@throttle
93
private async onDidRunWriteOperation(result: OperationResult): Promise<void> {
94
if (!this.repository.HEAD) {
95
this.logger.trace('[GitHistoryProvider][onDidRunWriteOperation] repository.HEAD is undefined');
96
this._currentHistoryItemRef = this._currentHistoryItemRemoteRef = this._currentHistoryItemBaseRef = undefined;
97
this._onDidChangeCurrentHistoryItemRefs.fire();
98
99
return;
100
}
101
102
// Refs (alphabetically)
103
const historyItemRefs = this.repository.refs
104
.map(ref => this.toSourceControlHistoryItemRef(ref))
105
.sort((a, b) => a.id.localeCompare(b.id));
106
107
const delta = deltaHistoryItemRefs(this._historyItemRefs, historyItemRefs);
108
this._historyItemRefs = historyItemRefs;
109
110
let historyItemRefId = '';
111
let historyItemRefName = '';
112
113
switch (this.repository.HEAD.type) {
114
case RefType.Head: {
115
if (this.repository.HEAD.name !== undefined) {
116
// Branch
117
historyItemRefId = `refs/heads/${this.repository.HEAD.name}`;
118
historyItemRefName = this.repository.HEAD.name;
119
120
// Remote
121
if (this.repository.HEAD.upstream) {
122
if (this.repository.HEAD.upstream.remote === '.') {
123
// Local branch
124
this._currentHistoryItemRemoteRef = {
125
id: `refs/heads/${this.repository.HEAD.upstream.name}`,
126
name: this.repository.HEAD.upstream.name,
127
revision: this.repository.HEAD.upstream.commit,
128
icon: new ThemeIcon('git-branch')
129
};
130
} else {
131
// Remote branch
132
this._currentHistoryItemRemoteRef = {
133
id: `refs/remotes/${this.repository.HEAD.upstream.remote}/${this.repository.HEAD.upstream.name}`,
134
name: `${this.repository.HEAD.upstream.remote}/${this.repository.HEAD.upstream.name}`,
135
revision: this.repository.HEAD.upstream.commit,
136
icon: new ThemeIcon('cloud')
137
};
138
}
139
} else {
140
this._currentHistoryItemRemoteRef = undefined;
141
}
142
143
// Base
144
if (this._HEAD?.name !== this.repository.HEAD.name) {
145
// Compute base if the branch has changed
146
const mergeBase = await this.resolveHEADMergeBase();
147
148
this._currentHistoryItemBaseRef = mergeBase && mergeBase.name && mergeBase.remote &&
149
(mergeBase.remote !== this.repository.HEAD.upstream?.remote ||
150
mergeBase.name !== this.repository.HEAD.upstream?.name) ? {
151
id: `refs/remotes/${mergeBase.remote}/${mergeBase.name}`,
152
name: `${mergeBase.remote}/${mergeBase.name}`,
153
revision: mergeBase.commit,
154
icon: new ThemeIcon('cloud')
155
} : undefined;
156
} else {
157
// Update base revision if it has changed
158
const mergeBaseModified = delta.modified
159
.find(ref => ref.id === this._currentHistoryItemBaseRef?.id);
160
161
if (this._currentHistoryItemBaseRef && mergeBaseModified) {
162
this._currentHistoryItemBaseRef = {
163
...this._currentHistoryItemBaseRef,
164
revision: mergeBaseModified.revision
165
};
166
}
167
}
168
} else {
169
// Detached commit
170
historyItemRefId = this.repository.HEAD.commit ?? '';
171
historyItemRefName = this.repository.HEAD.commit ?? '';
172
173
this._currentHistoryItemRemoteRef = undefined;
174
this._currentHistoryItemBaseRef = undefined;
175
}
176
break;
177
}
178
case RefType.Tag: {
179
// Tag
180
historyItemRefId = `refs/tags/${this.repository.HEAD.name}`;
181
historyItemRefName = this.repository.HEAD.name ?? this.repository.HEAD.commit ?? '';
182
183
this._currentHistoryItemRemoteRef = undefined;
184
this._currentHistoryItemBaseRef = undefined;
185
break;
186
}
187
}
188
189
// Update context keys for HEAD
190
if (this._HEAD?.ahead !== this.repository.HEAD?.ahead) {
191
commands.executeCommand('setContext', 'git.currentHistoryItemIsAhead', (this.repository.HEAD?.ahead ?? 0) > 0);
192
}
193
if (this._HEAD?.behind !== this.repository.HEAD?.behind) {
194
commands.executeCommand('setContext', 'git.currentHistoryItemIsBehind', (this.repository.HEAD?.behind ?? 0) > 0);
195
}
196
197
this._HEAD = this.repository.HEAD;
198
199
this._currentHistoryItemRef = {
200
id: historyItemRefId,
201
name: historyItemRefName,
202
revision: this.repository.HEAD.commit,
203
icon: new ThemeIcon('target'),
204
};
205
206
this._onDidChangeCurrentHistoryItemRefs.fire();
207
this.logger.trace(`[GitHistoryProvider][onDidRunWriteOperation] currentHistoryItemRef: ${JSON.stringify(this._currentHistoryItemRef)}`);
208
this.logger.trace(`[GitHistoryProvider][onDidRunWriteOperation] currentHistoryItemRemoteRef: ${JSON.stringify(this._currentHistoryItemRemoteRef)}`);
209
this.logger.trace(`[GitHistoryProvider][onDidRunWriteOperation] currentHistoryItemBaseRef: ${JSON.stringify(this._currentHistoryItemBaseRef)}`);
210
211
// Auto-fetch
212
const silent = result.operation.kind === OperationKind.Fetch && result.operation.showProgress === false;
213
this._onDidChangeHistoryItemRefs.fire({ ...delta, silent });
214
215
const deltaLog = {
216
added: delta.added.map(ref => ref.id),
217
modified: delta.modified.map(ref => ref.id),
218
removed: delta.removed.map(ref => ref.id),
219
silent
220
};
221
this.logger.trace(`[GitHistoryProvider][onDidRunWriteOperation] historyItemRefs: ${JSON.stringify(deltaLog)}`);
222
}
223
224
async provideHistoryItemRefs(historyItemRefs: string[] | undefined): Promise<SourceControlHistoryItemRef[]> {
225
const refs = await this.repository.getRefs({ pattern: historyItemRefs });
226
227
const branches: SourceControlHistoryItemRef[] = [];
228
const remoteBranches: SourceControlHistoryItemRef[] = [];
229
const tags: SourceControlHistoryItemRef[] = [];
230
231
for (const ref of refs) {
232
switch (ref.type) {
233
case RefType.RemoteHead:
234
remoteBranches.push(this.toSourceControlHistoryItemRef(ref));
235
break;
236
case RefType.Tag:
237
tags.push(this.toSourceControlHistoryItemRef(ref));
238
break;
239
default:
240
branches.push(this.toSourceControlHistoryItemRef(ref));
241
break;
242
}
243
}
244
245
return [...branches, ...remoteBranches, ...tags];
246
}
247
248
async provideHistoryItems(options: SourceControlHistoryOptions, token: CancellationToken): Promise<SourceControlHistoryItem[]> {
249
if (!this.currentHistoryItemRef || !options.historyItemRefs) {
250
return [];
251
}
252
253
// Deduplicate refNames
254
const refNames = Array.from(new Set<string>(options.historyItemRefs));
255
256
let logOptions: LogOptions = { refNames, shortStats: true };
257
258
try {
259
if (options.limit === undefined || typeof options.limit === 'number') {
260
logOptions = { ...logOptions, maxEntries: options.limit ?? 50 };
261
} else if (typeof options.limit.id === 'string') {
262
// Get the common ancestor commit, and commits
263
const commit = await this.repository.getCommit(options.limit.id);
264
const commitParentId = commit.parents.length > 0 ? commit.parents[0] : await this.repository.getEmptyTree();
265
266
logOptions = { ...logOptions, range: `${commitParentId}..` };
267
}
268
269
if (typeof options.skip === 'number') {
270
logOptions = { ...logOptions, skip: options.skip };
271
}
272
273
const commits = typeof options.filterText === 'string' && options.filterText !== ''
274
? await this._searchHistoryItems(options.filterText.trim(), logOptions, token)
275
: await this.repository.log({ ...logOptions, silent: true }, token);
276
277
if (token.isCancellationRequested) {
278
return [];
279
}
280
281
// Avatars
282
const avatarQuery = {
283
commits: commits.map(c => ({
284
hash: c.hash,
285
authorName: c.authorName,
286
authorEmail: c.authorEmail
287
} satisfies AvatarQueryCommit)),
288
size: 20
289
} satisfies AvatarQuery;
290
291
const commitAvatars = await provideSourceControlHistoryItemAvatar(
292
this.historyItemDetailProviderRegistry, this.repository, avatarQuery);
293
294
const remoteHoverCommands = await provideSourceControlHistoryItemHoverCommands(this.historyItemDetailProviderRegistry, this.repository) ?? [];
295
296
await ensureEmojis();
297
298
const historyItems: SourceControlHistoryItem[] = [];
299
for (const commit of commits) {
300
const message = emojify(commit.message);
301
const messageWithLinks = await provideSourceControlHistoryItemMessageLinks(
302
this.historyItemDetailProviderRegistry, this.repository, message) ?? message;
303
304
const avatarUrl = commitAvatars?.get(commit.hash);
305
const references = this._resolveHistoryItemRefs(commit);
306
307
const commands: Command[][] = [
308
getHoverCommitHashCommands(Uri.file(this.repository.root), commit.hash),
309
processHoverRemoteCommands(remoteHoverCommands, commit.hash)
310
];
311
312
const tooltip = getHistoryItemHover(avatarUrl, commit.authorName, commit.authorEmail, commit.authorDate ?? commit.commitDate, messageWithLinks, commit.shortStat, commands);
313
314
historyItems.push({
315
id: commit.hash,
316
parentIds: commit.parents,
317
subject: subject(message),
318
message: messageWithLinks,
319
author: commit.authorName,
320
authorEmail: commit.authorEmail,
321
authorIcon: avatarUrl ? Uri.parse(avatarUrl) : new ThemeIcon('account'),
322
displayId: truncate(commit.hash, this.commitShortHashLength, false),
323
timestamp: commit.authorDate?.getTime(),
324
statistics: commit.shortStat ?? { files: 0, insertions: 0, deletions: 0 },
325
references: references.length !== 0 ? references : undefined,
326
tooltip
327
} satisfies SourceControlHistoryItem);
328
}
329
330
return historyItems;
331
} catch (err) {
332
this.logger.error(`[GitHistoryProvider][provideHistoryItems] Failed to get history items with options '${JSON.stringify(options)}': ${err}`);
333
return [];
334
}
335
}
336
337
async provideHistoryItemChanges(historyItemId: string, historyItemParentId: string | undefined): Promise<SourceControlHistoryItemChange[]> {
338
historyItemParentId = historyItemParentId ?? await this.repository.getEmptyTree();
339
340
const historyItemChangesUri: Uri[] = [];
341
const historyItemChanges: SourceControlHistoryItemChange[] = [];
342
const changes = await this.repository.diffBetweenWithStats(historyItemParentId, historyItemId);
343
344
for (const change of changes) {
345
const historyItemUri = change.uri.with({
346
query: `ref=${historyItemId}`
347
});
348
349
// History item change
350
historyItemChanges.push({
351
uri: historyItemUri,
352
...toMultiFileDiffEditorUris(change, historyItemParentId, historyItemId)
353
} satisfies SourceControlHistoryItemChange);
354
355
// History item change decoration
356
const letter = Resource.getStatusLetter(change.status);
357
const tooltip = Resource.getStatusText(change.status);
358
const color = Resource.getStatusColor(change.status);
359
const fileDecoration = new FileDecoration(letter, tooltip, color);
360
this.historyItemDecorations.set(historyItemUri.toString(), fileDecoration);
361
362
historyItemChangesUri.push(historyItemUri);
363
}
364
365
this._onDidChangeDecorations.fire(historyItemChangesUri);
366
return historyItemChanges;
367
}
368
369
async resolveHistoryItem(historyItemId: string, token: CancellationToken): Promise<SourceControlHistoryItem | undefined> {
370
try {
371
const commit = await this.repository.getCommit(historyItemId);
372
373
if (!commit || token.isCancellationRequested) {
374
return undefined;
375
}
376
377
// Avatars
378
const avatarQuery = {
379
commits: [{
380
hash: commit.hash,
381
authorName: commit.authorName,
382
authorEmail: commit.authorEmail
383
} satisfies AvatarQueryCommit],
384
size: 20
385
} satisfies AvatarQuery;
386
387
const commitAvatars = await provideSourceControlHistoryItemAvatar(
388
this.historyItemDetailProviderRegistry, this.repository, avatarQuery);
389
390
await ensureEmojis();
391
392
const message = emojify(commit.message);
393
const messageWithLinks = await provideSourceControlHistoryItemMessageLinks(
394
this.historyItemDetailProviderRegistry, this.repository, message) ?? message;
395
396
const newLineIndex = message.indexOf('\n');
397
const subject = newLineIndex !== -1
398
? `${truncate(message, newLineIndex, false)}`
399
: message;
400
401
const avatarUrl = commitAvatars?.get(commit.hash);
402
const references = this._resolveHistoryItemRefs(commit);
403
404
return {
405
id: commit.hash,
406
parentIds: commit.parents,
407
subject,
408
message: messageWithLinks,
409
author: commit.authorName,
410
authorEmail: commit.authorEmail,
411
authorIcon: avatarUrl ? Uri.parse(avatarUrl) : new ThemeIcon('account'),
412
displayId: truncate(commit.hash, this.commitShortHashLength, false),
413
timestamp: commit.authorDate?.getTime(),
414
statistics: commit.shortStat ?? { files: 0, insertions: 0, deletions: 0 },
415
references: references.length !== 0 ? references : undefined
416
} satisfies SourceControlHistoryItem;
417
} catch (err) {
418
this.logger.error(`[GitHistoryProvider][resolveHistoryItem] Failed to resolve history item '${historyItemId}': ${err}`);
419
return undefined;
420
}
421
}
422
423
async resolveHistoryItemChatContext(historyItemId: string): Promise<string | undefined> {
424
try {
425
const changes = await this.repository.showChanges(historyItemId);
426
return changes;
427
} catch (err) {
428
this.logger.error(`[GitHistoryProvider][resolveHistoryItemChatContext] Failed to resolve history item '${historyItemId}': ${err}`);
429
}
430
431
return undefined;
432
}
433
434
async resolveHistoryItemChangeRangeChatContext(historyItemId: string, historyItemParentId: string, path: string, token: CancellationToken): Promise<string | undefined> {
435
try {
436
const changes = await this.repository.showChangesBetween(historyItemParentId, historyItemId, path);
437
438
if (token.isCancellationRequested) {
439
return undefined;
440
}
441
442
return `Output of git log -p ${historyItemParentId}..${historyItemId} -- ${path}:\n\n${changes}`;
443
} catch (err) {
444
this.logger.error(`[GitHistoryProvider][resolveHistoryItemChangeRangeChatContext] Failed to resolve history item change range '${historyItemId}' for '${path}': ${err}`);
445
}
446
447
return undefined;
448
}
449
450
async resolveHistoryItemRefsCommonAncestor(historyItemRefs: string[]): Promise<string | undefined> {
451
try {
452
if (historyItemRefs.length === 0) {
453
// TODO@lszomoru - log
454
return undefined;
455
} else if (historyItemRefs.length === 1 && historyItemRefs[0] === this.currentHistoryItemRef?.id) {
456
// Remote
457
if (this.currentHistoryItemRemoteRef) {
458
const ancestor = await this.repository.getMergeBase(historyItemRefs[0], this.currentHistoryItemRemoteRef.id);
459
return ancestor;
460
}
461
462
// Base
463
if (this.currentHistoryItemBaseRef) {
464
const ancestor = await this.repository.getMergeBase(historyItemRefs[0], this.currentHistoryItemBaseRef.id);
465
return ancestor;
466
}
467
468
// First commit
469
const commits = await this.repository.log({ maxParents: 0, refNames: ['HEAD'] });
470
if (commits.length > 0) {
471
return commits[0].hash;
472
}
473
} else if (historyItemRefs.length > 1) {
474
const ancestor = await this.repository.getMergeBase(historyItemRefs[0], historyItemRefs[1], ...historyItemRefs.slice(2));
475
return ancestor;
476
}
477
}
478
catch (err) {
479
this.logger.error(`[GitHistoryProvider][resolveHistoryItemRefsCommonAncestor] Failed to resolve common ancestor for ${historyItemRefs.join(',')}: ${err}`);
480
}
481
482
return undefined;
483
}
484
485
provideFileDecoration(uri: Uri): FileDecoration | undefined {
486
return this.historyItemDecorations.get(uri.toString());
487
}
488
489
private _resolveHistoryItemRefs(commit: Commit): SourceControlHistoryItemRef[] {
490
const references: SourceControlHistoryItemRef[] = [];
491
492
for (const ref of commit.refNames) {
493
if (ref === 'refs/remotes/origin/HEAD') {
494
continue;
495
}
496
497
switch (true) {
498
case ref.startsWith('HEAD -> refs/heads/'):
499
references.push({
500
id: ref.substring('HEAD -> '.length),
501
name: ref.substring('HEAD -> refs/heads/'.length),
502
revision: commit.hash,
503
category: l10n.t('branches'),
504
icon: new ThemeIcon('target')
505
});
506
break;
507
case ref.startsWith('refs/heads/'):
508
references.push({
509
id: ref,
510
name: ref.substring('refs/heads/'.length),
511
revision: commit.hash,
512
category: l10n.t('branches'),
513
icon: new ThemeIcon('git-branch')
514
});
515
break;
516
case ref.startsWith('refs/remotes/'):
517
references.push({
518
id: ref,
519
name: ref.substring('refs/remotes/'.length),
520
revision: commit.hash,
521
category: l10n.t('remote branches'),
522
icon: new ThemeIcon('cloud')
523
});
524
break;
525
case ref.startsWith('tag: refs/tags/'):
526
references.push({
527
id: ref.substring('tag: '.length),
528
name: ref.substring('tag: refs/tags/'.length),
529
revision: commit.hash,
530
category: l10n.t('tags'),
531
icon: new ThemeIcon('tag')
532
});
533
break;
534
}
535
}
536
537
return references.sort(compareSourceControlHistoryItemRef);
538
}
539
540
private async resolveHEADMergeBase(): Promise<Branch | undefined> {
541
try {
542
if (this.repository.HEAD?.type !== RefType.Head || !this.repository.HEAD?.name) {
543
return undefined;
544
}
545
546
const mergeBase = await this.repository.getBranchBase(this.repository.HEAD.name);
547
return mergeBase;
548
} catch (err) {
549
this.logger.error(`[GitHistoryProvider][resolveHEADMergeBase] Failed to resolve merge base for ${this.repository.HEAD?.name}: ${err}`);
550
return undefined;
551
}
552
}
553
554
private async _searchHistoryItems(filterText: string, options: LogOptions, token: CancellationToken): Promise<Commit[]> {
555
if (token.isCancellationRequested) {
556
return [];
557
}
558
559
const commits = new Map<string, Commit>();
560
561
// Search by author and commit message in parallel
562
const [authorResults, grepResults] = await Promise.all([
563
this.repository.log({ ...options, refNames: undefined, author: filterText, silent: true }, token),
564
this.repository.log({ ...options, refNames: undefined, grep: filterText, silent: true }, token)
565
]);
566
567
for (const commit of [...authorResults, ...grepResults]) {
568
if (!commits.has(commit.hash)) {
569
commits.set(commit.hash, commit);
570
}
571
}
572
573
return Array.from(commits.values()).slice(0, options.maxEntries ?? 50);
574
}
575
576
private toSourceControlHistoryItemRef(ref: Ref): SourceControlHistoryItemRef {
577
switch (ref.type) {
578
case RefType.RemoteHead:
579
return {
580
id: `refs/remotes/${ref.name}`,
581
name: ref.name ?? '',
582
description: ref.commit ? l10n.t('Remote branch at {0}', truncate(ref.commit, this.commitShortHashLength, false)) : undefined,
583
revision: ref.commit,
584
icon: new ThemeIcon('cloud'),
585
category: l10n.t('remote branches')
586
};
587
case RefType.Tag:
588
return {
589
id: `refs/tags/${ref.name}`,
590
name: ref.name ?? '',
591
description: ref.commit ? l10n.t('Tag at {0}', truncate(ref.commit, this.commitShortHashLength, false)) : undefined,
592
revision: ref.commit,
593
icon: new ThemeIcon('tag'),
594
category: l10n.t('tags')
595
};
596
default:
597
return {
598
id: `refs/heads/${ref.name}`,
599
name: ref.name ?? '',
600
description: ref.commit ? truncate(ref.commit, this.commitShortHashLength, false) : undefined,
601
revision: ref.commit,
602
icon: new ThemeIcon('git-branch'),
603
category: l10n.t('branches')
604
};
605
}
606
}
607
608
dispose(): void {
609
dispose(this.disposables);
610
}
611
}
612
613