SCM - consistently render date on the right in the repositories view (#280870)

SCM - scaffold artifact timestamp
This commit is contained in:
Ladislau Szomoru
2025-12-03 07:09:25 +00:00
committed by GitHub
parent b03e656cfa
commit 1027a1aad0
6 changed files with 92 additions and 21 deletions

View File

@@ -4,16 +4,13 @@
*--------------------------------------------------------------------------------------------*/
import { LogOutputChannel, SourceControlArtifactProvider, SourceControlArtifactGroup, SourceControlArtifact, Event, EventEmitter, ThemeIcon, l10n, workspace, Uri, Disposable } from 'vscode';
import { dispose, filterEvent, fromNow, getStashDescription, IDisposable } from './util';
import { dispose, filterEvent, IDisposable } from './util';
import { Repository } from './repository';
import { Ref, RefType } from './api/git';
import { OperationKind } from './operation';
function getArtifactDescription(ref: Ref, shortCommitLength: number): string {
const segments: string[] = [];
if (ref.commitDetails?.commitDate) {
segments.push(fromNow(ref.commitDetails.commitDate));
}
if (ref.commit) {
segments.push(ref.commit.substring(0, shortCommitLength));
}
@@ -130,7 +127,8 @@ export class GitArtifactProvider implements SourceControlArtifactProvider, IDisp
description: getArtifactDescription(r, shortCommitLength),
icon: this.repository.HEAD?.type === RefType.Head && r.name === this.repository.HEAD?.name
? new ThemeIcon('target')
: new ThemeIcon('git-branch')
: new ThemeIcon('git-branch'),
timestamp: r.commitDetails?.commitDate?.getTime()
}));
} else if (group === 'tags') {
const refs = await this.repository
@@ -142,7 +140,8 @@ export class GitArtifactProvider implements SourceControlArtifactProvider, IDisp
description: getArtifactDescription(r, shortCommitLength),
icon: this.repository.HEAD?.type === RefType.Tag && r.name === this.repository.HEAD?.name
? new ThemeIcon('target')
: new ThemeIcon('tag')
: new ThemeIcon('tag'),
timestamp: r.commitDetails?.commitDate?.getTime()
}));
} else if (group === 'stashes') {
const stashes = await this.repository.getStashes();
@@ -150,8 +149,9 @@ export class GitArtifactProvider implements SourceControlArtifactProvider, IDisp
return stashes.map(s => ({
id: `stash@{${s.index}}`,
name: s.description,
description: getStashDescription(s),
icon: new ThemeIcon('git-stash')
description: s.branchName,
icon: new ThemeIcon('git-stash'),
timestamp: s.commitDate?.getTime()
}));
}
} catch (err) {

View File

@@ -1765,6 +1765,7 @@ export interface SCMArtifactDto {
readonly name: string;
readonly description?: string;
readonly icon?: UriComponents | { light: UriComponents; dark: UriComponents } | ThemeIcon;
readonly timestamp?: number;
}
export interface MainThreadSCMShape extends IDisposable {

View File

@@ -585,6 +585,33 @@
white-space: nowrap;
}
.scm-repositories-view .scm-artifact .timestamp-container {
flex-shrink: 0;
margin-left: 2px;
margin-right: 4px;
opacity: 0.5;
}
.scm-repositories-view .scm-artifact .timestamp-container.duplicate {
height: 22px;
min-width: 6px;
border-left: 1px solid currentColor;
opacity: 0.25;
.timestamp {
display: none;
}
}
.scm-repositories-view .monaco-list .monaco-list-row:hover .scm-artifact .timestamp-container.duplicate {
border-left: 0;
opacity: 0.5;
.timestamp {
display: block;
}
}
/* History item hover */
.monaco-hover.history-item-hover .history-item-hover-container > .rendered-markdown:first-child > p {

View File

@@ -46,6 +46,7 @@ import { IAsyncDataTreeViewState, ITreeCompressionDelegate } from '../../../../b
import { Codicon } from '../../../../base/common/codicons.js';
import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js';
import { IActionViewItemProvider } from '../../../../base/browser/ui/actionbar/actionbar.js';
import { fromNow } from '../../../../base/common/date.js';
type TreeElement = ISCMRepository | SCMArtifactGroupTreeElement | SCMArtifactTreeElement | IResourceNode<SCMArtifactTreeElement, SCMArtifactGroupTreeElement>;
@@ -136,6 +137,8 @@ class ArtifactGroupRenderer implements ICompressibleTreeRenderer<SCMArtifactGrou
interface ArtifactTemplate {
readonly icon: HTMLElement;
readonly label: IconLabel;
readonly timestampContainer: HTMLElement;
readonly timestamp: HTMLElement;
readonly actionBar: WorkbenchToolBar;
readonly elementDisposables: DisposableStore;
readonly templateDisposable: IDisposable;
@@ -162,10 +165,13 @@ class ArtifactRenderer implements ICompressibleTreeRenderer<SCMArtifactTreeEleme
const icon = append(element, $('.icon'));
const label = new IconLabel(element, { supportIcons: false });
const timestampContainer = append(element, $('.timestamp-container'));
const timestamp = append(timestampContainer, $('.timestamp'));
const actionsContainer = append(element, $('.actions'));
const actionBar = new WorkbenchToolBar(actionsContainer, { actionViewItemProvider: this.actionViewItemProvider }, this._menuService, this._contextKeyService, this._contextMenuService, this._keybindingService, this._commandService, this._telemetryService);
return { icon, label, actionBar, elementDisposables: new DisposableStore(), templateDisposable: combinedDisposable(label, actionBar) };
return { icon, label, timestampContainer, timestamp, actionBar, elementDisposables: new DisposableStore(), templateDisposable: combinedDisposable(label, actionBar) };
}
renderElement(nodeOrElement: ITreeNode<SCMArtifactTreeElement | IResourceNode<SCMArtifactTreeElement, SCMArtifactGroupTreeElement>, FuzzyScore>, index: number, templateData: ArtifactTemplate): void {
@@ -186,10 +192,18 @@ class ArtifactRenderer implements ICompressibleTreeRenderer<SCMArtifactTreeEleme
? artifact.name.split('/').pop() ?? artifact.name
: artifact.name;
templateData.label.setLabel(artifactLabel, artifact.description);
templateData.timestamp.textContent = artifact.timestamp ? fromNow(artifact.timestamp) : '';
templateData.timestampContainer.classList.toggle('duplicate', artifactOrFolder.hideTimestamp);
templateData.timestampContainer.style.display = '';
} else if (isSCMArtifactNode(artifactOrFolder)) {
// Folder
templateData.icon.className = `icon ${ThemeIcon.asClassName(Codicon.folder)}`;
templateData.label.setLabel(basename(artifactOrFolder.uri));
templateData.timestamp.textContent = '';
templateData.timestampContainer.classList.remove('duplicate');
templateData.timestampContainer.style.display = 'none';
}
// Actions
@@ -211,10 +225,18 @@ class ArtifactRenderer implements ICompressibleTreeRenderer<SCMArtifactTreeEleme
: '';
templateData.label.setLabel(artifact.name, artifact.description);
templateData.timestamp.textContent = artifact.timestamp ? fromNow(artifact.timestamp) : '';
templateData.timestampContainer.classList.toggle('duplicate', artifactOrFolder.hideTimestamp);
templateData.timestampContainer.style.display = '';
} else if (isSCMArtifactNode(artifactOrFolder)) {
// Folder
templateData.icon.className = `icon ${ThemeIcon.asClassName(Codicon.folder)}`;
templateData.label.setLabel(artifactOrFolder.uri.fsPath.substring(1));
templateData.timestamp.textContent = '';
templateData.timestampContainer.classList.remove('duplicate');
templateData.timestampContainer.style.display = 'none';
}
// Actions
@@ -300,13 +322,29 @@ class RepositoryTreeDataSource extends Disposable implements IAsyncDataSource<IS
if (inputOrElement.artifactGroup.supportsFolders) {
// Resource tree for artifacts
const artifactsTree = new ResourceTree<SCMArtifactTreeElement, SCMArtifactGroupTreeElement>(inputOrElement);
for (const artifact of artifacts) {
artifactsTree.add(URI.from({
scheme: 'scm-artifact', path: artifact.name
}), {
for (let index = 0; index < artifacts.length; index++) {
const artifact = artifacts[index];
const artifactUri = URI.from({ scheme: 'scm-artifact', path: artifact.name });
const artifactBasename = artifact.id.lastIndexOf('/') > 0
? artifact.id.substring(0, artifact.id.lastIndexOf('/'))
: artifact.id;
const prevArtifact = index > 0 ? artifacts[index - 1] : undefined;
const prevArtifactBasename = prevArtifact && prevArtifact.id.lastIndexOf('/') > 0
? prevArtifact.id.substring(0, prevArtifact.id.lastIndexOf('/'))
: prevArtifact?.id;
const hideTimestamp = index > 0 &&
artifact.timestamp !== undefined &&
prevArtifact?.timestamp !== undefined &&
artifactBasename === prevArtifactBasename &&
fromNow(prevArtifact.timestamp) === fromNow(artifact.timestamp);
artifactsTree.add(artifactUri, {
repository,
group: inputOrElement.artifactGroup,
artifact,
hideTimestamp,
type: 'artifact'
});
}
@@ -315,14 +353,16 @@ class RepositoryTreeDataSource extends Disposable implements IAsyncDataSource<IS
}
// Flat list of artifacts
return artifacts.map(artifact => (
{
repository,
group: inputOrElement.artifactGroup,
artifact,
type: 'artifact'
} satisfies SCMArtifactTreeElement
));
return artifacts.map((artifact, index, artifacts) => ({
repository,
group: inputOrElement.artifactGroup,
artifact,
hideTimestamp: index > 0 &&
artifact.timestamp !== undefined &&
artifacts[index - 1].timestamp !== undefined &&
fromNow(artifacts[index - 1].timestamp!) === fromNow(artifact.timestamp),
type: 'artifact'
} satisfies SCMArtifactTreeElement));
} else if (isSCMArtifactNode(inputOrElement)) {
return Iterable.map(inputOrElement.children,
node => node.element && node.childrenCount === 0 ? node.element : node);

View File

@@ -26,6 +26,7 @@ export interface ISCMArtifact {
readonly name: string;
readonly description?: string;
readonly icon?: URI | { light: URI; dark: URI } | ThemeIcon;
readonly timestamp?: number;
}
export interface SCMArtifactGroupTreeElement {
@@ -38,5 +39,6 @@ export interface SCMArtifactTreeElement {
readonly repository: ISCMRepository;
readonly group: ISCMArtifactGroup;
readonly artifact: ISCMArtifact;
readonly hideTimestamp: boolean;
readonly type: 'artifact';
}

View File

@@ -29,5 +29,6 @@ declare module 'vscode' {
readonly name: string;
readonly description?: string;
readonly icon?: IconPath;
readonly timestamp?: number;
}
}