Skip to content

Commit

Permalink
SCM Graph - add branch picker (#227949)
Browse files Browse the repository at this point in the history
* WIP - saving my work

* Extract HistoryItemRef picker

* Extract Repository picker

* Improve history item ref picker rendering

* Refactor color map

* Refresh the graph when the filter changes

* Push minor fix
  • Loading branch information
lszomoru authored Sep 9, 2024
1 parent 884cfb1 commit 3ab41c2
Show file tree
Hide file tree
Showing 10 changed files with 534 additions and 161 deletions.
106 changes: 88 additions & 18 deletions extensions/git/src/historyProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------------*/


import { Disposable, Event, EventEmitter, FileDecoration, FileDecorationProvider, SourceControlHistoryItem, SourceControlHistoryItemChange, SourceControlHistoryItemGroup, SourceControlHistoryOptions, SourceControlHistoryProvider, ThemeIcon, Uri, window, LogOutputChannel, SourceControlHistoryItemLabel } from 'vscode';
import { Disposable, Event, EventEmitter, FileDecoration, FileDecorationProvider, SourceControlHistoryItem, SourceControlHistoryItemChange, SourceControlHistoryItemGroup, SourceControlHistoryOptions, SourceControlHistoryProvider, ThemeIcon, Uri, window, LogOutputChannel, SourceControlHistoryChangeEvent, SourceControlHistoryItemRef, l10n } from 'vscode';
import { Repository, Resource } from './repository';
import { IDisposable, dispose } from './util';
import { toGitUri } from './uri';
Expand All @@ -17,6 +17,9 @@ export class GitHistoryProvider implements SourceControlHistoryProvider, FileDec
private readonly _onDidChangeCurrentHistoryItemGroup = new EventEmitter<void>();
readonly onDidChangeCurrentHistoryItemGroup: Event<void> = this._onDidChangeCurrentHistoryItemGroup.event;

private readonly _onDidChangeHistory = new EventEmitter<SourceControlHistoryChangeEvent>();
readonly onDidChangeHistory: Event<SourceControlHistoryChangeEvent> = this._onDidChangeHistory.event;

private readonly _onDidChangeDecorations = new EventEmitter<Uri[]>();
readonly onDidChangeFileDecorations: Event<Uri[]> = this._onDidChangeDecorations.event;

Expand All @@ -28,12 +31,6 @@ export class GitHistoryProvider implements SourceControlHistoryProvider, FileDec
}

private historyItemDecorations = new Map<string, FileDecoration>();
private historyItemLabels = new Map<string, ThemeIcon>([
['HEAD -> refs/heads/', new ThemeIcon('target')],
['tag: refs/tags/', new ThemeIcon('tag')],
['refs/heads/', new ThemeIcon('git-branch')],
['refs/remotes/', new ThemeIcon('cloud')],
]);

private disposables: Disposable[] = [];

Expand Down Expand Up @@ -85,6 +82,51 @@ export class GitHistoryProvider implements SourceControlHistoryProvider, FileDec
this.logger.trace(`[GitHistoryProvider][onDidRunGitStatus] currentHistoryItemGroup: ${JSON.stringify(this.currentHistoryItemGroup)}`);
}

async provideHistoryItemRefs(): Promise<SourceControlHistoryItemRef[]> {
const refs = await this.repository.getRefs();

const branches: SourceControlHistoryItemRef[] = [];
const remoteBranches: SourceControlHistoryItemRef[] = [];
const tags: SourceControlHistoryItemRef[] = [];

for (const ref of refs) {
switch (ref.type) {
case RefType.RemoteHead:
remoteBranches.push({
id: `refs/remotes/${ref.remote}/${ref.name}`,
name: ref.name ?? '',
description: ref.commit ? l10n.t('Remote branch at {0}', ref.commit.substring(0, 8)) : undefined,
revision: ref.commit,
icon: new ThemeIcon('cloud'),
category: l10n.t('remote branches')
});
break;
case RefType.Tag:
tags.push({
id: `refs/tags/${ref.name}`,
name: ref.name ?? '',
description: ref.commit ? l10n.t('Tag at {0}', ref.commit.substring(0, 8)) : undefined,
revision: ref.commit,
icon: new ThemeIcon('tag'),
category: l10n.t('tags')
});
break;
default:
branches.push({
id: `refs/heads/${ref.name}`,
name: ref.name ?? '',
description: ref.commit ? ref.commit.substring(0, 8) : undefined,
revision: ref.commit,
icon: new ThemeIcon('git-branch'),
category: l10n.t('branches')
});
break;
}
}

return [...branches, ...remoteBranches, ...tags];
}

async provideHistoryItems(options: SourceControlHistoryOptions): Promise<SourceControlHistoryItem[]> {
if (!this.currentHistoryItemGroup || !options.historyItemGroupIds) {
return [];
Expand Down Expand Up @@ -115,7 +157,7 @@ export class GitHistoryProvider implements SourceControlHistoryProvider, FileDec
await ensureEmojis();

return commits.map(commit => {
const labels = this.resolveHistoryItemLabels(commit);
const references = this.resolveHistoryItemRefs(commit);

return {
id: commit.hash,
Expand All @@ -126,7 +168,7 @@ export class GitHistoryProvider implements SourceControlHistoryProvider, FileDec
displayId: commit.hash.substring(0, 8),
timestamp: commit.authorDate?.getTime(),
statistics: commit.shortStat ?? { files: 0, insertions: 0, deletions: 0 },
labels: labels.length !== 0 ? labels : undefined
references: references.length !== 0 ? references : undefined
};
});
} catch (err) {
Expand Down Expand Up @@ -208,19 +250,47 @@ export class GitHistoryProvider implements SourceControlHistoryProvider, FileDec
return this.historyItemDecorations.get(uri.toString());
}

private resolveHistoryItemLabels(commit: Commit): SourceControlHistoryItemLabel[] {
const labels: SourceControlHistoryItemLabel[] = [];

for (const label of commit.refNames) {
for (const [key, value] of this.historyItemLabels) {
if (label.startsWith(key)) {
labels.push({ title: label.substring(key.length), icon: value });
private resolveHistoryItemRefs(commit: Commit): SourceControlHistoryItemRef[] {
const references: SourceControlHistoryItemRef[] = [];

for (const ref of commit.refNames) {
switch (true) {
case ref.startsWith('HEAD -> refs/heads/'):
references.push({
id: ref.substring('HEAD -> '.length),
name: ref.substring('HEAD -> refs/heads/'.length),
revision: commit.hash,
icon: new ThemeIcon('target')
});
break;
case ref.startsWith('tag: refs/tags/'):
references.push({
id: ref.substring('tag: '.length),
name: ref.substring('tag: refs/tags/'.length),
revision: commit.hash,
icon: new ThemeIcon('tag')
});
break;
case ref.startsWith('refs/heads/'):
references.push({
id: ref,
name: ref.substring('refs/heads/'.length),
revision: commit.hash,
icon: new ThemeIcon('git-branch')
});
break;
case ref.startsWith('refs/remotes/'):
references.push({
id: ref,
name: ref.substring('refs/remotes/'.length),
revision: commit.hash,
icon: new ThemeIcon('cloud')
});
break;
}
}
}

return labels;
return references;
}

private async resolveHEADMergeBase(): Promise<Branch | undefined> {
Expand Down
62 changes: 57 additions & 5 deletions src/vs/workbench/api/browser/mainThreadSCM.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import { Barrier } from '../../../base/common/async.js';
import { URI, UriComponents } from '../../../base/common/uri.js';
import { Event, Emitter } from '../../../base/common/event.js';
import { observableValue, observableValueOpts } from '../../../base/common/observable.js';
import { derivedOpts, observableValue, observableValueOpts } from '../../../base/common/observable.js';
import { IDisposable, DisposableStore, combinedDisposable, dispose, Disposable } from '../../../base/common/lifecycle.js';
import { ISCMService, ISCMRepository, ISCMProvider, ISCMResource, ISCMResourceGroup, ISCMResourceDecorations, IInputValidation, ISCMViewService, InputValidationType, ISCMActionButtonDescriptor } from '../../contrib/scm/common/scm.js';
import { ExtHostContext, MainThreadSCMShape, ExtHostSCMShape, SCMProviderFeatures, SCMRawResourceSplices, SCMGroupFeatures, MainContext, SCMHistoryItemGroupDto, SCMHistoryItemDto } from '../common/extHost.protocol.js';
Expand All @@ -17,7 +17,7 @@ import { MarshalledId } from '../../../base/common/marshallingIds.js';
import { ThemeIcon } from '../../../base/common/themables.js';
import { IMarkdownString } from '../../../base/common/htmlContent.js';
import { IQuickDiffService, QuickDiffProvider } from '../../contrib/scm/common/quickDiff.js';
import { ISCMHistoryItem, ISCMHistoryItemChange, ISCMHistoryItemGroup, ISCMHistoryOptions, ISCMHistoryProvider } from '../../contrib/scm/common/history.js';
import { ISCMHistoryItem, ISCMHistoryItemChange, ISCMHistoryItemGroup, ISCMHistoryItemRef, ISCMHistoryOptions, ISCMHistoryProvider } from '../../contrib/scm/common/history.js';
import { ResourceTree } from '../../../base/common/resourceTree.js';
import { IUriIdentityService } from '../../../platform/uriIdentity/common/uriIdentity.js';
import { IWorkspaceContextService } from '../../../platform/workspace/common/workspace.js';
Expand All @@ -28,6 +28,8 @@ import { ITextModelContentProvider, ITextModelService } from '../../../editor/co
import { Schemas } from '../../../base/common/network.js';
import { ITextModel } from '../../../editor/common/model.js';
import { structuralEquals } from '../../../base/common/equals.js';
import { Codicon } from '../../../base/common/codicons.js';
import { historyItemGroupBase, historyItemGroupLocal, historyItemGroupRemote } from '../../contrib/scm/browser/scmHistory.js';

function getIconFromIconDto(iconDto?: UriComponents | { light: UriComponents; dark: UriComponents } | ThemeIcon): URI | { light: URI; dark: URI } | ThemeIcon | undefined {
if (iconDto === undefined) {
Expand All @@ -43,15 +45,15 @@ function getIconFromIconDto(iconDto?: UriComponents | { light: UriComponents; da
}

function toISCMHistoryItem(historyItemDto: SCMHistoryItemDto): ISCMHistoryItem {
const labels = historyItemDto.labels?.map(l => ({
title: l.title, icon: getIconFromIconDto(l.icon)
const references = historyItemDto.references?.map(r => ({
...r, icon: getIconFromIconDto(r.icon)
}));

const newLineIndex = historyItemDto.message.indexOf('\n');
const subject = newLineIndex === -1 ?
historyItemDto.message : `${historyItemDto.message.substring(0, newLineIndex)}\u2026`;

return { ...historyItemDto, subject, labels };
return { ...historyItemDto, subject, references };
}

class SCMInputBoxContentProvider extends Disposable implements ITextModelContentProvider {
Expand Down Expand Up @@ -171,12 +173,62 @@ class MainThreadSCMHistoryProvider implements ISCMHistoryProvider {
}, undefined);
get currentHistoryItemGroup() { return this._currentHistoryItemGroup; }

readonly currentHistoryItemRef = derivedOpts<ISCMHistoryItemRef | undefined>({
owner: this,
equalsFn: structuralEquals
}, reader => {
const currentHistoryItemGroup = this._currentHistoryItemGroup.read(reader);

return currentHistoryItemGroup ? {
id: currentHistoryItemGroup.id ?? '',
name: currentHistoryItemGroup.name,
revision: currentHistoryItemGroup.revision,
color: historyItemGroupLocal,
icon: Codicon.target,
} : undefined;
});

readonly currentHistoryItemRemoteRef = derivedOpts<ISCMHistoryItemRef | undefined>({
owner: this,
equalsFn: structuralEquals
}, reader => {
const currentHistoryItemGroup = this._currentHistoryItemGroup.read(reader);

return currentHistoryItemGroup?.remote ? {
id: currentHistoryItemGroup.remote.id ?? '',
name: currentHistoryItemGroup.remote.name,
revision: currentHistoryItemGroup.remote.revision,
color: historyItemGroupRemote,
icon: Codicon.cloud,
} : undefined;
});

readonly currentHistoryItemBaseRef = derivedOpts<ISCMHistoryItemRef | undefined>({
owner: this,
equalsFn: structuralEquals
}, reader => {
const currentHistoryItemGroup = this._currentHistoryItemGroup.read(reader);

return currentHistoryItemGroup?.base ? {
id: currentHistoryItemGroup.base.id ?? '',
name: currentHistoryItemGroup.base.name,
revision: currentHistoryItemGroup.base.revision,
color: historyItemGroupBase,
icon: Codicon.cloud,
} : undefined;
});

constructor(private readonly proxy: ExtHostSCMShape, private readonly handle: number) { }

async resolveHistoryItemGroupCommonAncestor(historyItemGroupIds: string[]): Promise<string | undefined> {
return this.proxy.$resolveHistoryItemGroupCommonAncestor(this.handle, historyItemGroupIds, CancellationToken.None);
}

async provideHistoryItemRefs(): Promise<ISCMHistoryItemRef[] | undefined> {
const historyItemRefs = await this.proxy.$provideHistoryItemRefs(this.handle, CancellationToken.None);
return historyItemRefs?.map(ref => ({ ...ref, icon: getIconFromIconDto(ref.icon) }));
}

async provideHistoryItems(options: ISCMHistoryOptions): Promise<ISCMHistoryItem[] | undefined> {
const historyItems = await this.proxy.$provideHistoryItems(this.handle, options, CancellationToken.None);
return historyItems?.map(historyItem => toISCMHistoryItem(historyItem));
Expand Down
15 changes: 11 additions & 4 deletions src/vs/workbench/api/common/extHost.protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1550,6 +1550,15 @@ export interface SCMHistoryItemGroupDto {
readonly remote?: Omit<Omit<SCMHistoryItemGroupDto, 'base'>, 'remote'>;
}

export interface SCMHistoryItemRefDto {
readonly id: string;
readonly name: string;
readonly revision?: string;
readonly category?: string;
readonly description?: string;
readonly icon?: UriComponents | { light: UriComponents; dark: UriComponents } | ThemeIcon;
}

export interface SCMHistoryItemDto {
readonly id: string;
readonly parentIds: string[];
Expand All @@ -1562,10 +1571,7 @@ export interface SCMHistoryItemDto {
readonly insertions: number;
readonly deletions: number;
};
readonly labels?: {
readonly title: string;
readonly icon?: UriComponents | { light: UriComponents; dark: UriComponents } | ThemeIcon;
}[];
readonly references?: SCMHistoryItemRefDto[];
}

export interface SCMHistoryItemChangeDto {
Expand Down Expand Up @@ -2358,6 +2364,7 @@ export interface ExtHostSCMShape {
$executeResourceCommand(sourceControlHandle: number, groupHandle: number, handle: number, preserveFocus: boolean): Promise<void>;
$validateInput(sourceControlHandle: number, value: string, cursorPosition: number): Promise<[string | IMarkdownString, number] | undefined>;
$setSelectedSourceControl(selectedSourceControlHandle: number | undefined): Promise<void>;
$provideHistoryItemRefs(sourceControlHandle: number, token: CancellationToken): Promise<SCMHistoryItemRefDto[] | undefined>;
$provideHistoryItems(sourceControlHandle: number, options: any, token: CancellationToken): Promise<SCMHistoryItemDto[] | undefined>;
$provideHistoryItemChanges(sourceControlHandle: number, historyItemId: string, historyItemParentId: string | undefined, token: CancellationToken): Promise<SCMHistoryItemChangeDto[] | undefined>;
$resolveHistoryItemGroupCommonAncestor(sourceControlHandle: number, historyItemGroupIds: string[], token: CancellationToken): Promise<string | undefined>;
Expand Down
15 changes: 11 additions & 4 deletions src/vs/workbench/api/common/extHostSCM.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { debounce } from '../../../base/common/decorators.js';
import { DisposableStore, IDisposable, MutableDisposable } from '../../../base/common/lifecycle.js';
import { asPromise } from '../../../base/common/async.js';
import { ExtHostCommands } from './extHostCommands.js';
import { MainContext, MainThreadSCMShape, SCMRawResource, SCMRawResourceSplice, SCMRawResourceSplices, IMainContext, ExtHostSCMShape, ICommandDto, MainThreadTelemetryShape, SCMGroupFeatures, SCMHistoryItemDto, SCMHistoryItemChangeDto } from './extHost.protocol.js';
import { MainContext, MainThreadSCMShape, SCMRawResource, SCMRawResourceSplice, SCMRawResourceSplices, IMainContext, ExtHostSCMShape, ICommandDto, MainThreadTelemetryShape, SCMGroupFeatures, SCMHistoryItemDto, SCMHistoryItemChangeDto, SCMHistoryItemRefDto } from './extHost.protocol.js';
import { sortedDiff, equals } from '../../../base/common/arrays.js';
import { comparePaths } from '../../../base/common/comparers.js';
import type * as vscode from 'vscode';
Expand Down Expand Up @@ -72,11 +72,11 @@ function getHistoryItemIconDto(icon: vscode.Uri | { light: vscode.Uri; dark: vsc
}

function toSCMHistoryItemDto(historyItem: vscode.SourceControlHistoryItem): SCMHistoryItemDto {
const labels = historyItem.labels?.map(l => ({
title: l.title, icon: getHistoryItemIconDto(l.icon)
const references = historyItem.references?.map(r => ({
...r, icon: getHistoryItemIconDto(r.icon)
}));

return { ...historyItem, labels };
return { ...historyItem, references };
}

function compareResourceThemableDecorations(a: vscode.SourceControlResourceThemableDecorations, b: vscode.SourceControlResourceThemableDecorations): number {
Expand Down Expand Up @@ -982,6 +982,13 @@ export class ExtHostSCM implements ExtHostSCMShape {
return await historyProvider?.resolveHistoryItemGroupCommonAncestor(historyItemGroupIds, token) ?? undefined;
}

async $provideHistoryItemRefs(sourceControlHandle: number, token: CancellationToken): Promise<SCMHistoryItemRefDto[] | undefined> {
const historyProvider = this._sourceControls.get(sourceControlHandle)?.historyProvider;
const historyItemRefs = await historyProvider?.provideHistoryItemRefs(token);

return historyItemRefs?.map(ref => ({ ...ref, icon: getHistoryItemIconDto(ref.icon) })) ?? undefined;
}

async $provideHistoryItems(sourceControlHandle: number, options: any, token: CancellationToken): Promise<SCMHistoryItemDto[] | undefined> {
const historyProvider = this._sourceControls.get(sourceControlHandle)?.historyProvider;
const historyItems = await historyProvider?.provideHistoryItems(options, token);
Expand Down
8 changes: 5 additions & 3 deletions src/vs/workbench/contrib/scm/browser/media/scm.css
Original file line number Diff line number Diff line change
Expand Up @@ -495,13 +495,15 @@
display: flex;
}

.monaco-toolbar .action-label.scm-graph-repository-picker {
.monaco-toolbar .action-label.scm-graph-repository-picker,
.monaco-toolbar .action-label.scm-graph-history-item-picker {
align-items: center;
font-weight: normal;
line-height: 16px;
}

.monaco-toolbar .action-label.scm-graph-repository-picker .codicon {
.monaco-toolbar .action-label.scm-graph-repository-picker .codicon,
.monaco-toolbar .action-label.scm-graph-history-item-picker .codicon {
font-size: 14px;
}

Expand Down Expand Up @@ -558,7 +560,7 @@

.scm-history-view .history-item-load-more .history-item-placeholder.shimmer .monaco-icon-label-container {
height: 18px;
background: var(--vscode-scm-historyItemDefaultLabelBackground);
background: var(--vscode-scmGraph-historyItemHoverDefaultLabelBackground);
border-radius: 2px;
opacity: 0.5;
}
Loading

0 comments on commit 3ab41c2

Please sign in to comment.