SCM - history view (#216629)

* Hacked up the starting point

* More progress

* More progress

* Simplified algo, basic support for colors

* Add the concept of secondary colors for merge commits

* Further optimizations of the algo

* Bug fixes to clean-up algo

* Add support for curved branches

* Keep track of nodes that are moved so that the second parent for the merge commit is rendered in the correct swimlane

* Account for offset when drawing a base commit

* Algo rework completed

* Revert some of the hacks that were put in place to quickly get going

* Add caching to the history items used in the graph
Remove more hacks that were in place

* Added initial tests

* More work so that incoming/outgoing works along history

* Uncomment more code

* Bug fixes to edge cases

* Experiment with a new rendering for curved branches

* Handle repository with a single commit

* Maintain swimlanes

* Fix condition

* Saving my changes

* More polish and clean-up

* Remove code that is not needed

* Revert change

* Revert more changes

* More fixes

* Rename interface

* One last minor change

* Pull request feedback

* More refactoring

* More pull request feedback

* Fix layering issues
This commit is contained in:
Ladislau Szomoru authored and GitHub committed 2024-06-19 19:45:06 +02:00
1 parent 1330e6d2b8
commit 484cd68eff
13 files changed
+1180 -36

No files matched your search

+1
View File
@@ -145,6 +145,7 @@ export interface LogOptions {
readonly sortByAuthorDate?: boolean;
readonly shortStats?: boolean;
readonly author?: string;
readonly refNames?: string[];
}
export interface CommitOptions {
+5
View File
@@ -1165,6 +1165,11 @@ export class Repository {
args.push(`--author="${options.author}"`);
}
if (options?.refNames) {
args.push('--topo-order');
args.push(...options.refNames);
}
if (options?.path) {
args.push('--', options.path);
}
+119 -5
View File
@@ -4,13 +4,14 @@
*--------------------------------------------------------------------------------------------*/
import { Disposable, Event, EventEmitter, FileDecoration, FileDecorationProvider, SourceControlHistoryItem, SourceControlHistoryItemChange, SourceControlHistoryItemGroup, SourceControlHistoryOptions, SourceControlHistoryProvider, ThemeIcon, Uri, window, LogOutputChannel } from 'vscode';
import { Disposable, Event, EventEmitter, FileDecoration, FileDecorationProvider, SourceControlHistoryItem, SourceControlHistoryItemChange, SourceControlHistoryItemGroup, SourceControlHistoryOptions, SourceControlHistoryProvider, ThemeIcon, Uri, window, LogOutputChannel, SourceControlHistoryItemLabel } from 'vscode';
import { Repository, Resource } from './repository';
import { IDisposable, dispose, filterEvent } from './util';
import { toGitUri } from './uri';
import { Branch, RefType, UpstreamRef } from './api/git';
import { emojify, ensureEmojis } from './emoji';
import { Operation } from './operation';
import { Commit } from './git';
export class GitHistoryProvider implements SourceControlHistoryProvider, FileDecorationProvider, IDisposable {
@@ -112,6 +113,47 @@ export class GitHistoryProvider implements SourceControlHistoryProvider, FileDec
return historyItems;
}
async provideHistoryItems2(options: SourceControlHistoryOptions): Promise<SourceControlHistoryItem[]> {
if (!this.currentHistoryItemGroup || !options.historyItemGroupIds) {
return [];
}
// Deduplicate refNames
const refNames = new Set<string>(options.historyItemGroupIds);
// Get the merge base of the refNames
const refsMergeBase = await this.resolveHistoryItemGroupsMergeBase(refNames);
if (!refsMergeBase) {
return [];
}
// Get the commits
const commits = await this.repository.log({ range: `${refsMergeBase}^..`, refNames: Array.from(refNames) });
await ensureEmojis();
const historyItems: SourceControlHistoryItem[] = [];
historyItems.push(...commits.map(commit => {
const newLineIndex = commit.message.indexOf('\n');
const subject = newLineIndex !== -1 ? commit.message.substring(0, newLineIndex) : commit.message;
const labels = this.resolveHistoryItemLabels(commit, refNames);
return {
id: commit.hash,
parentIds: commit.parents,
message: emojify(subject),
author: commit.authorName,
icon: new ThemeIcon('git-commit'),
timestamp: commit.authorDate?.getTime(),
statistics: commit.shortStat ?? { files: 0, insertions: 0, deletions: 0 },
labels: labels.length !== 0 ? labels : undefined
};
}));
return historyItems;
}
async provideHistoryItemSummary(historyItemId: string, historyItemParentId: string | undefined): Promise<SourceControlHistoryItem> {
if (!historyItemParentId) {
const commit = await this.repository.getCommit(historyItemId);
@@ -159,9 +201,23 @@ export class GitHistoryProvider implements SourceControlHistoryProvider, FileDec
return historyItemChanges;
}
async resolveHistoryItemGroupBase(historyItemGroupId: string): Promise<SourceControlHistoryItemGroup | undefined> {
// Base (config -> reflog -> default)
const remoteBranch = await this.repository.getBranchBase(historyItemGroupId);
if (!remoteBranch?.remote || !remoteBranch?.name || !remoteBranch?.commit || remoteBranch?.type !== RefType.RemoteHead) {
this.logger.info(`GitHistoryProvider:resolveHistoryItemGroupBase - Failed to resolve history item group base for '${historyItemGroupId}'`);
return undefined;
}
return {
id: `refs/remotes/${remoteBranch.remote}/${remoteBranch.name}`,
name: `${remoteBranch.remote}/${remoteBranch.name}`,
};
}
async resolveHistoryItemGroupCommonAncestor(historyItemId1: string, historyItemId2: string | undefined): Promise<{ id: string; ahead: number; behind: number } | undefined> {
if (!historyItemId2) {
const upstreamRef = await this.resolveHistoryItemGroupBase(historyItemId1);
const upstreamRef = await this.resolveHistoryItemGroupUpstreamOrBase(historyItemId1);
if (!upstreamRef) {
this.logger.info(`GitHistoryProvider:resolveHistoryItemGroupCommonAncestor - Failed to resolve history item group base for '${historyItemId1}'`);
return undefined;
@@ -191,7 +247,65 @@ export class GitHistoryProvider implements SourceControlHistoryProvider, FileDec
return this.historyItemDecorations.get(uri.toString());
}
private async resolveHistoryItemGroupBase(historyItemId: string): Promise<UpstreamRef | undefined> {
private async resolveHistoryItemGroupsMergeBase(refNames: Set<string>): Promise<string | undefined> {
let refsMergeBase: string | undefined = undefined;
for (const refName of refNames) {
if (refsMergeBase === undefined) {
const commit = await this.repository.revParse(refName);
refsMergeBase = commit ?? refName;
continue;
}
const newMergeBase = await this.repository.getMergeBase(refsMergeBase, refName);
refsMergeBase = newMergeBase ?? refsMergeBase;
}
return refsMergeBase;
}
private resolveHistoryItemLabels(commit: Commit, refNames: Set<string>): SourceControlHistoryItemLabel[] {
const labels: SourceControlHistoryItemLabel[] = [];
for (const label of commit.refNames) {
if (label === 'origin/HEAD' || label === '') {
continue;
}
if (label.startsWith('HEAD -> ')) {
labels.push(
{
title: label.substring(8),
icon: new ThemeIcon('git-branch')
}
);
continue;
}
if (refNames.has(label)) {
if (label.startsWith('tag: ')) {
labels.push({
title: label.substring(5),
icon: new ThemeIcon('tag')
});
} else if (label.startsWith('origin/')) {
labels.push({
title: label,
icon: new ThemeIcon('cloud')
});
} else {
labels.push({
title: label,
icon: new ThemeIcon('git-branch')
});
}
}
}
return labels;
}
private async resolveHistoryItemGroupUpstreamOrBase(historyItemId: string): Promise<UpstreamRef | undefined> {
try {
// Upstream
const branch = await this.repository.getBranch(historyItemId);
@@ -202,7 +316,7 @@ export class GitHistoryProvider implements SourceControlHistoryProvider, FileDec
// Base (config -> reflog -> default)
const remoteBranch = await this.repository.getBranchBase(historyItemId);
if (!remoteBranch?.remote || !remoteBranch?.name || !remoteBranch?.commit || remoteBranch?.type !== RefType.RemoteHead) {
this.logger.info(`GitHistoryProvider:resolveHistoryItemGroupBase - Failed to resolve history item group base for '${historyItemId}'`);
this.logger.info(`GitHistoryProvider:resolveHistoryItemGroupUpstreamOrBase - Failed to resolve history item group base for '${historyItemId}'`);
return undefined;
}
@@ -213,7 +327,7 @@ export class GitHistoryProvider implements SourceControlHistoryProvider, FileDec
};
}
catch (err) {
this.logger.error(`GitHistoryProvider:resolveHistoryItemGroupBase - Failed to get branch base for '${historyItemId}': ${err.message}`);
this.logger.error(`GitHistoryProvider:resolveHistoryItemGroupUpstreamOrBase - Failed to get branch base for '${historyItemId}': ${err.message}`);
}
return undefined;
+19 -3
View File
@@ -9,7 +9,7 @@ import { Event, Emitter } from 'vs/base/common/event';
import { observableValue } from 'vs/base/common/observable';
import { IDisposable, DisposableStore, combinedDisposable, dispose, Disposable } from 'vs/base/common/lifecycle';
import { ISCMService, ISCMRepository, ISCMProvider, ISCMResource, ISCMResourceGroup, ISCMResourceDecorations, IInputValidation, ISCMViewService, InputValidationType, ISCMActionButtonDescriptor } from 'vs/workbench/contrib/scm/common/scm';
import { ExtHostContext, MainThreadSCMShape, ExtHostSCMShape, SCMProviderFeatures, SCMRawResourceSplices, SCMGroupFeatures, MainContext, SCMHistoryItemGroupDto } from '../common/extHost.protocol';
import { ExtHostContext, MainThreadSCMShape, ExtHostSCMShape, SCMProviderFeatures, SCMRawResourceSplices, SCMGroupFeatures, MainContext, SCMHistoryItemGroupDto, SCMHistoryItemDto } from '../common/extHost.protocol';
import { Command } from 'vs/editor/common/languages';
import { extHostNamedCustomer, IExtHostContext } from 'vs/workbench/services/extensions/common/extHostCustomers';
import { CancellationToken } from 'vs/base/common/cancellation';
@@ -42,6 +42,13 @@ function getIconFromIconDto(iconDto?: UriComponents | { light: UriComponents; da
}
}
function toISCMHistoryItem(historyItemDto: SCMHistoryItemDto): ISCMHistoryItem {
const icon = getIconFromIconDto(historyItemDto.icon);
const labels = historyItemDto.labels?.map(l => ({ title: l.title, icon: getIconFromIconDto(l.icon) }));
return { ...historyItemDto, icon, labels };
}
class SCMInputBoxContentProvider extends Disposable implements ITextModelContentProvider {
constructor(
textModelService: ITextModelService,
@@ -170,18 +177,27 @@ class MainThreadSCMHistoryProvider implements ISCMHistoryProvider {
constructor(private readonly proxy: ExtHostSCMShape, private readonly handle: number) { }
async resolveHistoryItemGroupBase(historyItemGroupId: string): Promise<ISCMHistoryItemGroup | undefined> {
return this.proxy.$resolveHistoryItemGroupBase(this.handle, historyItemGroupId, CancellationToken.None);
}
async resolveHistoryItemGroupCommonAncestor(historyItemGroupId1: string, historyItemGroupId2: string | undefined): Promise<{ id: string; ahead: number; behind: number } | undefined> {
return this.proxy.$resolveHistoryItemGroupCommonAncestor(this.handle, historyItemGroupId1, historyItemGroupId2, CancellationToken.None);
}
async provideHistoryItems(historyItemGroupId: string, options: ISCMHistoryOptions): Promise<ISCMHistoryItem[] | undefined> {
const historyItems = await this.proxy.$provideHistoryItems(this.handle, historyItemGroupId, options, CancellationToken.None);
return historyItems?.map(historyItem => ({ ...historyItem, icon: getIconFromIconDto(historyItem.icon) }));
return historyItems?.map(historyItem => toISCMHistoryItem(historyItem));
}
async provideHistoryItems2(options: ISCMHistoryOptions): Promise<ISCMHistoryItem[] | undefined> {
const historyItems = await this.proxy.$provideHistoryItems2(this.handle, options, CancellationToken.None);
return historyItems?.map(historyItem => toISCMHistoryItem(historyItem));
}
async provideHistoryItemSummary(historyItemId: string, historyItemParentId: string | undefined): Promise<ISCMHistoryItem | undefined> {
const historyItem = await this.proxy.$provideHistoryItemSummary(this.handle, historyItemId, historyItemParentId, CancellationToken.None);
return historyItem ? { ...historyItem, icon: getIconFromIconDto(historyItem.icon) } : undefined;
return historyItem ? toISCMHistoryItem(historyItem) : undefined;
}
async provideHistoryItemChanges(historyItemId: string, historyItemParentId: string | undefined): Promise<ISCMHistoryItemChange[] | undefined> {
@@ -1525,6 +1525,15 @@ export interface SCMHistoryItemDto {
readonly author?: string;
readonly icon?: UriComponents | { light: UriComponents; dark: UriComponents } | ThemeIcon;
readonly timestamp?: number;
readonly statistics?: {
readonly files: number;
readonly insertions: number;
readonly deletions: number;
};
readonly labels?: {
readonly title: string;
readonly icon?: UriComponents | { light: UriComponents; dark: UriComponents } | ThemeIcon;
}[];
}
export interface SCMHistoryItemChangeDto {
@@ -2317,8 +2326,10 @@ export interface ExtHostSCMShape {
$validateInput(sourceControlHandle: number, value: string, cursorPosition: number): Promise<[string | IMarkdownString, number] | undefined>;
$setSelectedSourceControl(selectedSourceControlHandle: number | undefined): Promise<void>;
$provideHistoryItems(sourceControlHandle: number, historyItemGroupId: string, options: any, token: CancellationToken): Promise<SCMHistoryItemDto[] | undefined>;
$provideHistoryItems2(sourceControlHandle: number, options: any, token: CancellationToken): Promise<SCMHistoryItemDto[] | undefined>;
$provideHistoryItemSummary(sourceControlHandle: number, historyItemId: string, historyItemParentId: string | undefined, token: CancellationToken): Promise<SCMHistoryItemDto | undefined>;
$provideHistoryItemChanges(sourceControlHandle: number, historyItemId: string, historyItemParentId: string | undefined, token: CancellationToken): Promise<SCMHistoryItemChangeDto[] | undefined>;
$resolveHistoryItemGroupBase(sourceControlHandle: number, historyItemGroupId: string, token: CancellationToken): Promise<SCMHistoryItemGroupDto | undefined>;
$resolveHistoryItemGroupCommonAncestor(sourceControlHandle: number, historyItemGroupId1: string, historyItemGroupId2: string | undefined, token: CancellationToken): Promise<{ id: string; ahead: number; behind: number } | undefined>;
}
+30 -11
View File
@@ -11,7 +11,7 @@ import { debounce } from 'vs/base/common/decorators';
import { DisposableStore, IDisposable, MutableDisposable } from 'vs/base/common/lifecycle';
import { asPromise } from 'vs/base/common/async';
import { ExtHostCommands } from 'vs/workbench/api/common/extHostCommands';
import { MainContext, MainThreadSCMShape, SCMRawResource, SCMRawResourceSplice, SCMRawResourceSplices, IMainContext, ExtHostSCMShape, ICommandDto, MainThreadTelemetryShape, SCMGroupFeatures, SCMHistoryItemDto, SCMHistoryItemChangeDto } from './extHost.protocol';
import { MainContext, MainThreadSCMShape, SCMRawResource, SCMRawResourceSplice, SCMRawResourceSplices, IMainContext, ExtHostSCMShape, ICommandDto, MainThreadTelemetryShape, SCMGroupFeatures, SCMHistoryItemDto, SCMHistoryItemChangeDto, SCMHistoryItemGroupDto } from './extHost.protocol';
import { sortedDiff, equals } from 'vs/base/common/arrays';
import { comparePaths } from 'vs/base/common/comparers';
import type * as vscode from 'vscode';
@@ -58,19 +58,26 @@ function getIconResource(decorations?: vscode.SourceControlResourceThemableDecor
}
}
function getHistoryItemIconDto(historyItem: vscode.SourceControlHistoryItem): UriComponents | { light: UriComponents; dark: UriComponents } | ThemeIcon | undefined {
if (!historyItem.icon) {
function getHistoryItemIconDto(icon: vscode.Uri | { light: vscode.Uri; dark: vscode.Uri } | vscode.ThemeIcon | undefined): UriComponents | { light: UriComponents; dark: UriComponents } | ThemeIcon | undefined {
if (!icon) {
return undefined;
} else if (URI.isUri(historyItem.icon)) {
return historyItem.icon;
} else if (ThemeIcon.isThemeIcon(historyItem.icon)) {
return historyItem.icon;
} else if (URI.isUri(icon)) {
return icon;
} else if (ThemeIcon.isThemeIcon(icon)) {
return icon;
} else {
const icon = historyItem.icon as { light: URI; dark: URI };
return { light: icon.light, dark: icon.dark };
const iconDto = icon as { light: URI; dark: URI };
return { light: iconDto.light, dark: iconDto.dark };
}
}
function toSCMHistoryItemDto(historyItem: vscode.SourceControlHistoryItem): SCMHistoryItemDto {
const icon = getHistoryItemIconDto(historyItem.icon);
const labels = historyItem.labels?.map(l => ({ title: l.title, icon: getHistoryItemIconDto(l.icon) }));
return { ...historyItem, icon, labels };
}
function compareResourceThemableDecorations(a: vscode.SourceControlResourceThemableDecorations, b: vscode.SourceControlResourceThemableDecorations): number {
if (!a.iconPath && !b.iconPath) {
return 0;
@@ -963,6 +970,11 @@ export class ExtHostSCM implements ExtHostSCMShape {
return Promise.resolve(undefined);
}
async $resolveHistoryItemGroupBase(sourceControlHandle: number, historyItemGroupId: string, token: CancellationToken): Promise<SCMHistoryItemGroupDto | undefined> {
const historyProvider = this._sourceControls.get(sourceControlHandle)?.historyProvider;
return await historyProvider?.resolveHistoryItemGroupBase(historyItemGroupId, token) ?? undefined;
}
async $resolveHistoryItemGroupCommonAncestor(sourceControlHandle: number, historyItemGroupId1: string, historyItemGroupId2: string | undefined, token: CancellationToken): Promise<{ id: string; ahead: number; behind: number } | undefined> {
const historyProvider = this._sourceControls.get(sourceControlHandle)?.historyProvider;
return await historyProvider?.resolveHistoryItemGroupCommonAncestor(historyItemGroupId1, historyItemGroupId2, token) ?? undefined;
@@ -972,7 +984,14 @@ export class ExtHostSCM implements ExtHostSCMShape {
const historyProvider = this._sourceControls.get(sourceControlHandle)?.historyProvider;
const historyItems = await historyProvider?.provideHistoryItems(historyItemGroupId, options, token);
return historyItems?.map(item => ({ ...item, icon: getHistoryItemIconDto(item) })) ?? undefined;
return historyItems?.map(item => toSCMHistoryItemDto(item)) ?? undefined;
}
async $provideHistoryItems2(sourceControlHandle: number, options: any, token: CancellationToken): Promise<SCMHistoryItemDto[] | undefined> {
const historyProvider = this._sourceControls.get(sourceControlHandle)?.historyProvider;
const historyItems = await historyProvider?.provideHistoryItems2(options, token);
return historyItems?.map(item => toSCMHistoryItemDto(item)) ?? undefined;
}
async $provideHistoryItemSummary(sourceControlHandle: number, historyItemId: string, historyItemParentId: string | undefined, token: CancellationToken): Promise<SCMHistoryItemDto | undefined> {
@@ -982,7 +1001,7 @@ export class ExtHostSCM implements ExtHostSCMShape {
}
const historyItem = await historyProvider.provideHistoryItemSummary(historyItemId, historyItemParentId, token);
return historyItem ? { ...historyItem, icon: getHistoryItemIconDto(historyItem) } : undefined;
return historyItem ? toSCMHistoryItemDto(historyItem) : undefined;
}
async $provideHistoryItemChanges(sourceControlHandle: number, historyItemId: string, historyItemParentId: string | undefined, token: CancellationToken): Promise<SCMHistoryItemChangeDto[] | undefined> {
@@ -129,6 +129,31 @@
align-items: center;
}
.scm-view .monaco-list-row .history-item > .graph-container {
display: flex;
flex-shrink: 0;
height: 22px;
}
.scm-view .monaco-list-row .history-item > .graph-container > .graph > circle {
stroke: var(--vscode-sideBar-background);
}
.scm-view .monaco-list-row .history-item > .label-container {
display: flex;
opacity: 0.75;
flex-shrink: 0;
gap: 4px;
}
.scm-view .monaco-list-row .history-item > .label-container > .codicon {
font-size: 14px;
border: 1px solid var(--vscode-scm-historyItemStatisticsBorder);
border-radius: 2px;
margin: 1px 0;
padding: 2px
}
.scm-view .monaco-list-row .history-item .stats-container {
display: flex;
font-size: 11px;
@@ -0,0 +1,238 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { lastOrDefault } from 'vs/base/common/arrays';
import { deepClone } from 'vs/base/common/objects';
import { ISCMHistoryItem, ISCMHistoryItemGraphNode, ISCMHistoryItemViewModel } from 'vs/workbench/contrib/scm/common/history';
const SWIMLANE_HEIGHT = 22;
const SWIMLANE_WIDTH = 11;
const CIRCLE_RADIUS = 4;
const SWIMLANE_CURVE_RADIUS = 5;
const graphColors = ['#007ACC', '#BC3FBC', '#BF8803', '#CC6633', '#F14C4C', '#16825D'];
function createPath(stroke: string): SVGPathElement {
const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
path.setAttribute('fill', 'none');
path.setAttribute('stroke', stroke);
path.setAttribute('stroke-width', '1px');
path.setAttribute('stroke-linecap', 'round');
return path;
}
function drawCircle(index: number, radius: number, fill: string): SVGCircleElement {
const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
circle.setAttribute('cx', `${SWIMLANE_WIDTH * (index + 1)}`);
circle.setAttribute('cy', `${SWIMLANE_WIDTH}`);
circle.setAttribute('r', `${radius}`);
circle.setAttribute('fill', fill);
return circle;
}
function drawVerticalLine(x1: number, y1: number, y2: number, color: string): SVGPathElement {
const path = createPath(color);
path.setAttribute('d', `M ${x1} ${y1} V ${y2}`);
return path;
}
function findLastIndex(nodes: ISCMHistoryItemGraphNode[], id: string): number {
for (let i = nodes.length - 1; i >= 0; i--) {
if (nodes[i].id === id) {
return i;
}
}
return -1;
}
export function renderSCMHistoryItemGraph(historyItemViewModel: ISCMHistoryItemViewModel): SVGElement {
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
svg.classList.add('graph');
const historyItem = historyItemViewModel.historyItem;
const inputSwimlanes = historyItemViewModel.inputSwimlanes;
const outputSwimlanes = historyItemViewModel.outputSwimlanes;
const inputIndex = inputSwimlanes.findIndex(node => node.id === historyItem.id);
const outputIndex = historyItem.parentIds.length === 0 ? -1 : outputSwimlanes.findIndex(node => node.id === historyItem.parentIds[0]);
const circleIndex = inputIndex !== -1 ? inputIndex : inputSwimlanes.length;
const circleColorIndex = inputIndex !== -1 ? inputSwimlanes[inputIndex].color : outputSwimlanes[circleIndex]?.color ?? 0;
for (let index = 0; index < inputSwimlanes.length; index++) {
const node = inputSwimlanes[index];
const color = graphColors[inputSwimlanes[index].color];
// Not the current commit
if (node.id !== historyItem.id) {
if (index < outputSwimlanes.length && node.id === outputSwimlanes[index].id) {
// Draw |
const path = drawVerticalLine(SWIMLANE_WIDTH * (index + 1), 0, SWIMLANE_HEIGHT, color);
svg.append(path);
} else {
const d: string[] = [];
const path = createPath(color);
// Draw |
d.push(`M ${SWIMLANE_WIDTH * (index + 1)} 0`);
d.push(`V 6`);
// Draw /
d.push(`A ${SWIMLANE_CURVE_RADIUS} ${SWIMLANE_CURVE_RADIUS} 0 0 1 ${(SWIMLANE_WIDTH * (index + 1)) - SWIMLANE_CURVE_RADIUS} ${SWIMLANE_HEIGHT / 2}`);
// Start walking backwards from the current index and
// find the first occurrence in the output swimlanes
// array
let nodeOutputIndex = -1;
for (let j = Math.min(index, outputSwimlanes.length) - 1; j >= 0; j--) {
if (outputSwimlanes[j].id === node.id) {
nodeOutputIndex = j;
break;
}
}
// Draw -
d.push(`H ${(SWIMLANE_WIDTH * (nodeOutputIndex + 1)) + SWIMLANE_CURVE_RADIUS}`);
// Draw /
d.push(`A ${SWIMLANE_CURVE_RADIUS} ${SWIMLANE_CURVE_RADIUS} 0 0 0 ${SWIMLANE_WIDTH * (nodeOutputIndex + 1)} ${(SWIMLANE_HEIGHT / 2) + SWIMLANE_CURVE_RADIUS}`);
// Draw |
d.push(`V ${SWIMLANE_HEIGHT}`);
path.setAttribute('d', d.join(' '));
svg.append(path);
}
continue;
}
// Base commit
if (index !== circleIndex) {
const d: string[] = [];
const path = createPath(color);
// Draw /
d.push(`M ${SWIMLANE_WIDTH * (index + 1)} 0`);
d.push(`A ${SWIMLANE_WIDTH} ${SWIMLANE_WIDTH} 0 0 1 ${SWIMLANE_WIDTH * (index)} ${SWIMLANE_WIDTH}`);
// Draw -
d.push(`H ${SWIMLANE_WIDTH * (circleIndex + 1)}`);
path.setAttribute('d', d.join(' '));
svg.append(path);
}
}
// Add remaining parent(s)
for (let i = 1; i < historyItem.parentIds.length; i++) {
const parentOutputIndex = findLastIndex(outputSwimlanes, historyItem.parentIds[i]);
if (parentOutputIndex === -1) {
continue;
}
// Draw -\
const d: string[] = [];
const path = createPath(graphColors[outputSwimlanes[parentOutputIndex].color]);
// Draw \
d.push(`M ${SWIMLANE_WIDTH * parentOutputIndex} ${SWIMLANE_HEIGHT / 2}`);
d.push(`A ${SWIMLANE_WIDTH} ${SWIMLANE_WIDTH} 0 0 1 ${SWIMLANE_WIDTH * (parentOutputIndex + 1)} ${SWIMLANE_HEIGHT}`);
// Draw -
d.push(`M ${SWIMLANE_WIDTH * parentOutputIndex} ${SWIMLANE_HEIGHT / 2}`);
d.push(`H ${SWIMLANE_WIDTH * (circleIndex + 1)} `);
path.setAttribute('d', d.join(' '));
svg.append(path);
}
// Draw | to circle
if (inputIndex !== -1) {
const path = drawVerticalLine(SWIMLANE_WIDTH * (circleIndex + 1), 0, SWIMLANE_HEIGHT / 2, graphColors[circleColorIndex]);
svg.append(path);
}
// Draw | from circle
if (outputIndex !== -1) {
const path = drawVerticalLine(SWIMLANE_WIDTH * (circleIndex + 1), SWIMLANE_HEIGHT / 2, SWIMLANE_HEIGHT, graphColors[circleColorIndex]);
svg.append(path);
}
// Draw *
if (historyItem.parentIds.length > 1) {
// Multi-parent node
const circleOuter = drawCircle(circleIndex, CIRCLE_RADIUS + 1, graphColors[circleColorIndex]);
svg.append(circleOuter);
const circleInner = drawCircle(circleIndex, CIRCLE_RADIUS - 1, graphColors[circleColorIndex]);
svg.append(circleInner);
} else {
// Node
const circle = drawCircle(circleIndex, CIRCLE_RADIUS, graphColors[circleColorIndex]);
svg.append(circle);
}
// Set dimensions
svg.style.height = `${SWIMLANE_HEIGHT}px`;
svg.style.width = `${SWIMLANE_WIDTH * (Math.max(inputSwimlanes.length, outputSwimlanes.length, 1) + 1)}px`;
return svg;
}
export function toISCMHistoryItemViewModelArray(historyItems: ISCMHistoryItem[]): ISCMHistoryItemViewModel[] {
let colorIndex = -1;
const viewModels: ISCMHistoryItemViewModel[] = [];
for (let index = 0; index < historyItems.length; index++) {
const historyItem = historyItems[index];
const outputSwimlanesFromPreviousItem = lastOrDefault(viewModels)?.outputSwimlanes ?? [];
const inputSwimlanes = outputSwimlanesFromPreviousItem.map(i => deepClone(i));
const outputSwimlanes: ISCMHistoryItemGraphNode[] = [];
if (historyItem.parentIds.length > 0) {
let firstParentAdded = false;
// Add first parent to the output
for (const node of inputSwimlanes) {
if (node.id === historyItem.id) {
if (!firstParentAdded) {
outputSwimlanes.push({
...deepClone(node),
id: historyItem.parentIds[0]
});
firstParentAdded = true;
}
continue;
}
outputSwimlanes.push(deepClone(node));
}
// Add unprocessed parent(s) to the output
for (let i = firstParentAdded ? 1 : 0; i < historyItem.parentIds.length; i++) {
colorIndex = colorIndex < graphColors.length - 1 ? colorIndex + 1 : 1;
outputSwimlanes.push({
id: historyItem.parentIds[i],
color: colorIndex
});
}
}
viewModels.push({
historyItem,
inputSwimlanes,
outputSwimlanes,
});
}
return viewModels;
}
@@ -10,7 +10,7 @@ import { IDisposable, Disposable, DisposableStore, combinedDisposable, dispose,
import { ViewPane, IViewPaneOptions, ViewAction } from 'vs/workbench/browser/parts/views/viewPane';
import { append, $, Dimension, asCSSUrl, trackFocus, clearNode, prepend, isPointerEvent, isActiveElement } from 'vs/base/browser/dom';
import { IListVirtualDelegate, IIdentityProvider } from 'vs/base/browser/ui/list/list';
import { ISCMHistoryItem, ISCMHistoryItemChange, ISCMHistoryProviderCacheEntry, SCMHistoryItemChangeTreeElement, SCMHistoryItemGroupTreeElement, SCMHistoryItemTreeElement, SCMViewSeparatorElement } from 'vs/workbench/contrib/scm/common/history';
import { ISCMHistoryItem, ISCMHistoryItemChange, ISCMHistoryItemViewModel, SCMHistoryItemViewModelTreeElement, ISCMHistoryProviderCacheEntry, SCMHistoryItemChangeTreeElement, SCMHistoryItemGroupTreeElement, SCMHistoryItemTreeElement, SCMViewSeparatorElement } from 'vs/workbench/contrib/scm/common/history';
import { ISCMResourceGroup, ISCMResource, InputValidationType, ISCMRepository, ISCMInput, IInputValidation, ISCMViewService, ISCMViewVisibleRepositoryChangeEvent, ISCMService, SCMInputChangeReason, VIEW_PANE_ID, ISCMActionButton, ISCMActionButtonDescriptor, ISCMRepositorySortKey, ISCMInputValueProviderContext, ISCMProvider } from 'vs/workbench/contrib/scm/common/scm';
import { ResourceLabels, IResourceLabel, IFileLabelOptions } from 'vs/workbench/browser/labels';
import { CountBadge } from 'vs/base/browser/ui/countBadge/countBadge';
@@ -24,7 +24,7 @@ import { MenuItemAction, IMenuService, registerAction2, MenuId, IAction2Options,
import { IAction, ActionRunner, Action, Separator, IActionRunner } from 'vs/base/common/actions';
import { ActionBar, IActionViewItemProvider } from 'vs/base/browser/ui/actionbar/actionbar';
import { IThemeService, IFileIconTheme } from 'vs/platform/theme/common/themeService';
import { isSCMResource, isSCMResourceGroup, connectPrimaryMenuToInlineActionBar, isSCMRepository, isSCMInput, collectContextMenuActions, getActionViewItemProvider, isSCMActionButton, isSCMViewService, isSCMHistoryItemGroupTreeElement, isSCMHistoryItemTreeElement, isSCMHistoryItemChangeTreeElement, toDiffEditorArguments, isSCMResourceNode, isSCMHistoryItemChangeNode, isSCMViewSeparator, connectPrimaryMenu } from './util';
import { isSCMResource, isSCMResourceGroup, connectPrimaryMenuToInlineActionBar, isSCMRepository, isSCMInput, collectContextMenuActions, getActionViewItemProvider, isSCMActionButton, isSCMViewService, isSCMHistoryItemGroupTreeElement, isSCMHistoryItemTreeElement, isSCMHistoryItemChangeTreeElement, toDiffEditorArguments, isSCMResourceNode, isSCMHistoryItemChangeNode, isSCMViewSeparator, connectPrimaryMenu, isSCMHistoryItemViewModelTreeElement } from './util';
import { WorkbenchCompressibleAsyncDataTree, IOpenEvent } from 'vs/platform/list/browser/listService';
import { IConfigurationService, ConfigurationTarget } from 'vs/platform/configuration/common/configuration';
import { disposableTimeout, Sequencer, ThrottledDelayer, Throttler } from 'vs/base/common/async';
@@ -102,7 +102,6 @@ import { CancellationTokenSource } from 'vs/base/common/cancellation';
import { DropdownWithPrimaryActionViewItem } from 'vs/platform/actions/browser/dropdownWithPrimaryActionViewItem';
import { clamp, rot } from 'vs/base/common/numbers';
import { ILogService } from 'vs/platform/log/common/log';
import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegateFactory';
import { MarkdownString } from 'vs/base/common/htmlContent';
import type { IManagedHover, IManagedHoverTooltipMarkdownString } from 'vs/base/browser/ui/hover/hover';
import { IHoverService } from 'vs/platform/hover/browser/hover';
@@ -110,6 +109,8 @@ import { OpenScmGroupAction } from 'vs/workbench/contrib/multiDiffEditor/browser
import { HoverController } from 'vs/editor/contrib/hover/browser/hoverController';
import { ITextModel } from 'vs/editor/common/model';
import { autorun } from 'vs/base/common/observable';
import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegateFactory';
import { renderSCMHistoryItemGraph, toISCMHistoryItemViewModelArray } from 'vs/workbench/contrib/scm/browser/scmHistory';
// type SCMResourceTreeNode = IResourceNode<ISCMResource, ISCMResourceGroup>;
// type SCMHistoryItemChangeResourceTreeNode = IResourceNode<SCMHistoryItemChangeTreeElement, SCMHistoryItemTreeElement>;
@@ -122,6 +123,7 @@ type TreeElement =
IResourceNode<ISCMResource, ISCMResourceGroup> |
SCMHistoryItemGroupTreeElement |
SCMHistoryItemTreeElement |
SCMHistoryItemViewModelTreeElement |
SCMHistoryItemChangeTreeElement |
IResourceNode<SCMHistoryItemChangeTreeElement, SCMHistoryItemTreeElement> |
SCMViewSeparatorElement;
@@ -1003,6 +1005,104 @@ class HistoryItemRenderer implements ICompressibleTreeRenderer<SCMHistoryItemTre
}
}
interface HistoryItem2Template {
readonly label: IconLabel;
readonly graphContainer: HTMLElement;
readonly labelContainer: HTMLElement;
readonly elementDisposables: DisposableStore;
readonly disposables: IDisposable;
}
class HistoryItem2Renderer implements ICompressibleTreeRenderer<SCMHistoryItemViewModelTreeElement, LabelFuzzyScore, HistoryItem2Template> {
static readonly TEMPLATE_ID = 'history-item-2';
get templateId(): string { return HistoryItem2Renderer.TEMPLATE_ID; }
constructor(
@IHoverService private readonly hoverService: IHoverService
) { }
renderTemplate(container: HTMLElement): HistoryItem2Template {
// hack
(container.parentElement!.parentElement!.querySelector('.monaco-tl-twistie')! as HTMLElement).classList.add('force-no-twistie');
const element = append(container, $('.history-item'));
const graphContainer = append(element, $('.graph-container'));
const iconLabel = new IconLabel(element, { supportIcons: true, supportHighlights: true, supportDescriptionHighlights: true });
const labelContainer = append(element, $('.label-container'));
element.appendChild(labelContainer);
return { graphContainer, label: iconLabel, labelContainer, elementDisposables: new DisposableStore(), disposables: new DisposableStore() };
}
renderElement(node: ITreeNode<SCMHistoryItemViewModelTreeElement, LabelFuzzyScore>, index: number, templateData: HistoryItem2Template, height: number | undefined): void {
const historyItemViewModel = node.element.historyItemViewModel;
const historyItem = historyItemViewModel.historyItem;
templateData.graphContainer.textContent = '';
templateData.graphContainer.appendChild(renderSCMHistoryItemGraph(historyItemViewModel));
const title = this.getTooltip(historyItemViewModel);
const [matches, descriptionMatches] = this.processMatches(historyItemViewModel, node.filterData);
templateData.label.setLabel(historyItem.message, undefined, { title, matches, descriptionMatches });
templateData.labelContainer.textContent = '';
if (historyItem.labels) {
for (const label of historyItem.labels) {
if (label.icon && ThemeIcon.isThemeIcon(label.icon)) {
const icon = append(templateData.labelContainer, $('div.label'));
icon.classList.add(...ThemeIcon.asClassNameArray(label.icon));
templateData.elementDisposables.add(this.hoverService.setupManagedHover(getDefaultHoverDelegate('element'), icon, label.title));
}
}
}
}
renderCompressedElements(node: ITreeNode<ICompressedTreeNode<SCMHistoryItemViewModelTreeElement>, LabelFuzzyScore>, index: number, templateData: HistoryItem2Template, height: number | undefined): void {
throw new Error('Should never happen since node is incompressible');
}
private getTooltip(historyItemViewModel: ISCMHistoryItemViewModel): IManagedHoverTooltipMarkdownString {
const historyItem = historyItemViewModel.historyItem;
const markdown = new MarkdownString('', { isTrusted: true, supportThemeIcons: true });
if (historyItem.author) {
markdown.appendMarkdown(`$(account) **${historyItem.author}**\n\n`);
}
if (historyItem.timestamp) {
const dateFormatter = new Intl.DateTimeFormat(platform.language, { year: 'numeric', month: 'long', day: 'numeric', hour: 'numeric', minute: 'numeric' });
markdown.appendMarkdown(`$(history) ${dateFormatter.format(historyItem.timestamp)}\n\n`);
}
markdown.appendMarkdown(historyItem.message);
return { markdown, markdownNotSupportedFallback: historyItem.message };
}
private processMatches(historyItemViewModel: ISCMHistoryItemViewModel, filterData: LabelFuzzyScore | undefined): [IMatch[] | undefined, IMatch[] | undefined] {
if (!filterData) {
return [undefined, undefined];
}
return [
historyItemViewModel.historyItem.message === filterData.label ? createMatches(filterData.score) : undefined,
historyItemViewModel.historyItem.author === filterData.label ? createMatches(filterData.score) : undefined
];
}
disposeElement(element: ITreeNode<SCMHistoryItemViewModelTreeElement, LabelFuzzyScore>, index: number, templateData: HistoryItem2Template, height: number | undefined): void {
templateData.elementDisposables.clear();
}
disposeTemplate(templateData: HistoryItem2Template): void {
templateData.disposables.dispose();
}
}
interface HistoryItemChangeTemplate {
readonly element: HTMLElement;
readonly name: HTMLElement;
@@ -1084,6 +1184,7 @@ class SeparatorRenderer implements ICompressibleTreeRenderer<SCMViewSeparatorEle
@IContextKeyService private readonly contextKeyService: IContextKeyService,
@IContextMenuService private readonly contextMenuService: IContextMenuService,
@IKeybindingService private readonly keybindingService: IKeybindingService,
@IConfigurationService private readonly configurationService: IConfigurationService,
@ICommandService private readonly commandService: ICommandService,
@IMenuService private readonly menuService: IMenuService,
@ITelemetryService private readonly telemetryService: ITelemetryService
@@ -1102,8 +1203,10 @@ class SeparatorRenderer implements ICompressibleTreeRenderer<SCMViewSeparatorEle
append(element, $('.separator'));
disposables.add(label);
const toolBar = new MenuWorkbenchToolBar(append(element, $('.actions')), MenuId.SCMChangesSeparator, { moreIcon: Codicon.gear }, this.menuService, this.contextKeyService, this.contextMenuService, this.keybindingService, this.commandService, this.telemetryService);
disposables.add(toolBar);
if (this.configurationService.getValue<boolean>('scm.showHistoryGraph') !== true) {
const toolBar = new MenuWorkbenchToolBar(append(element, $('.actions')), MenuId.SCMChangesSeparator, { moreIcon: Codicon.gear }, this.menuService, this.contextKeyService, this.contextMenuService, this.keybindingService, this.commandService, this.telemetryService);
disposables.add(toolBar);
}
return { label, disposables };
}
@@ -1150,6 +1253,8 @@ class ListDelegate implements IListVirtualDelegate<TreeElement> {
return HistoryItemGroupRenderer.TEMPLATE_ID;
} else if (isSCMHistoryItemTreeElement(element)) {
return HistoryItemRenderer.TEMPLATE_ID;
} else if (isSCMHistoryItemViewModelTreeElement(element)) {
return HistoryItem2Renderer.TEMPLATE_ID;
} else if (isSCMHistoryItemChangeTreeElement(element) || isSCMHistoryItemChangeNode(element)) {
return HistoryItemChangeRenderer.TEMPLATE_ID;
} else if (isSCMViewSeparator(element)) {
@@ -1230,6 +1335,10 @@ export class SCMTreeSorter implements ITreeSorter<TreeElement> {
return 0;
}
if (isSCMHistoryItemViewModelTreeElement(one)) {
return isSCMHistoryItemViewModelTreeElement(other) ? 0 : 1;
}
if (isSCMHistoryItemChangeTreeElement(one) || isSCMHistoryItemChangeNode(one)) {
// List
if (this.viewMode() === ViewMode.List) {
@@ -1314,6 +1423,11 @@ export class SCMTreeKeyboardNavigationLabelProvider implements ICompressibleKeyb
// the author. A match in the message takes precedence over
// a match in the author.
return [element.message, element.author];
} else if (isSCMHistoryItemViewModelTreeElement(element)) {
// For a history item we want to match both the message and
// the author. A match in the message takes precedence over
// a match in the author.
return [element.historyItemViewModel.historyItem.message, element.historyItemViewModel.historyItem.author];
} else if (isSCMViewSeparator(element)) {
return element.label;
} else {
@@ -1364,6 +1478,10 @@ function getSCMResourceId(element: TreeElement): string {
const historyItemGroup = element.historyItemGroup;
const provider = historyItemGroup.repository.provider;
return `historyItem:${provider.id}/${historyItemGroup.id}/${element.id}/${element.parentIds.join(',')}`;
} else if (isSCMHistoryItemViewModelTreeElement(element)) {
const provider = element.repository.provider;
const historyItem = element.historyItemViewModel.historyItem;
return `historyItem2:${provider.id}/${historyItem.id}/${historyItem.parentIds.join(',')}`;
} else if (isSCMHistoryItemChangeTreeElement(element)) {
const historyItem = element.historyItem;
const historyItemGroup = historyItem.historyItemGroup;
@@ -1414,6 +1532,9 @@ export class SCMAccessibilityProvider implements IListAccessibilityProvider<Tree
return element.ariaLabel ?? `${element.label.trim()}${element.description ? `, ${element.description}` : ''}`;
} else if (isSCMHistoryItemTreeElement(element)) {
return `${stripIcons(element.message).trim()}${element.author ? `, ${element.author}` : ''}`;
} else if (isSCMHistoryItemViewModelTreeElement(element)) {
const historyItem = element.historyItemViewModel.historyItem;
return `${stripIcons(historyItem.message).trim()}${historyItem.author ? `, ${historyItem.author}` : ''}`;
} else if (isSCMHistoryItemChangeTreeElement(element)) {
const result = [basename(element.uri)];
const path = this.labelService.getUriLabel(dirname(element.uri), { relative: true, noPrefix: true });
@@ -1488,7 +1609,7 @@ MenuRegistry.appendMenuItem(MenuId.SCMTitle, {
MenuRegistry.appendMenuItem(MenuId.SCMTitle, {
title: localize('scmChanges', "Incoming & Outgoing"),
submenu: Menus.ChangesSettings,
when: ContextKeyExpr.and(ContextKeyExpr.equals('view', VIEW_PANE_ID), ContextKeys.RepositoryCount.notEqualsTo(0)),
when: ContextKeyExpr.and(ContextKeyExpr.equals('view', VIEW_PANE_ID), ContextKeys.RepositoryCount.notEqualsTo(0), ContextKeyExpr.equals('config.scm.showHistoryGraph', true).negate()),
group: '0_view&sort',
order: 2
});
@@ -2895,7 +3016,8 @@ export class SCMViewPane extends ViewPane {
e.affectsConfiguration('scm.showActionButton') ||
e.affectsConfiguration('scm.showChangesSummary') ||
e.affectsConfiguration('scm.showIncomingChanges') ||
e.affectsConfiguration('scm.showOutgoingChanges'),
e.affectsConfiguration('scm.showOutgoingChanges') ||
e.affectsConfiguration('scm.showHistoryGraph'),
this.visibilityDisposables)
(() => this.updateChildren(), this, this.visibilityDisposables);
@@ -2973,6 +3095,7 @@ export class SCMViewPane extends ViewPane {
this.instantiationService.createInstance(ResourceRenderer, () => this.viewMode, this.listLabels, getActionViewItemProvider(this.instantiationService), resourceActionRunner),
this.instantiationService.createInstance(HistoryItemGroupRenderer, historyItemGroupActionRunner),
this.instantiationService.createInstance(HistoryItemRenderer, historyItemActionRunner, getActionViewItemProvider(this.instantiationService)),
this.instantiationService.createInstance(HistoryItem2Renderer),
this.instantiationService.createInstance(HistoryItemChangeRenderer, () => this.viewMode, this.listLabels),
this.instantiationService.createInstance(SeparatorRenderer)
],
@@ -3601,6 +3724,8 @@ class SCMTreeDataSource implements IAsyncDataSource<ISCMViewService, TreeElement
return true;
} else if (isSCMHistoryItemTreeElement(inputOrElement)) {
return true;
} else if (isSCMHistoryItemViewModelTreeElement(inputOrElement)) {
return false;
} else if (isSCMHistoryItemChangeTreeElement(inputOrElement)) {
return false;
} else if (isSCMViewSeparator(inputOrElement)) {
@@ -3667,6 +3792,17 @@ class SCMTreeDataSource implements IAsyncDataSource<ISCMViewService, TreeElement
children.push(...historyItemGroups);
// History items
const historyItems = await this.getHistoryItems2(inputOrElement);
if (historyItems.length > 0) {
const label = localize('historySeparatorHeader', "History");
const ariaLabel = localize('historySeparatorHeaderAriaLabel', "History");
children.push({ label, ariaLabel, repository: inputOrElement, type: 'separator' } satisfies SCMViewSeparatorElement);
}
children.push(...historyItems);
return children;
} else if (isSCMResourceGroup(inputOrElement)) {
if (this.viewMode() === ViewMode.List) {
@@ -3701,13 +3837,13 @@ class SCMTreeDataSource implements IAsyncDataSource<ISCMViewService, TreeElement
}
private async getHistoryItemGroups(element: ISCMRepository): Promise<SCMHistoryItemGroupTreeElement[]> {
const { showIncomingChanges, showOutgoingChanges } = this.getConfiguration();
const { showIncomingChanges, showOutgoingChanges, showHistoryGraph } = this.getConfiguration();
const scmProvider = element.provider;
const historyProvider = scmProvider.historyProvider;
const currentHistoryItemGroup = historyProvider?.currentHistoryItemGroup;
if (!historyProvider || !currentHistoryItemGroup || (showIncomingChanges === 'never' && showOutgoingChanges === 'never')) {
if (!historyProvider || !currentHistoryItemGroup || (showIncomingChanges === 'never' && showOutgoingChanges === 'never') || showHistoryGraph) {
return [];
}
@@ -3822,6 +3958,44 @@ class SCMTreeDataSource implements IAsyncDataSource<ISCMViewService, TreeElement
return children;
}
private async getHistoryItems2(element: ISCMRepository): Promise<SCMHistoryItemViewModelTreeElement[]> {
const { showHistoryGraph } = this.getConfiguration();
const historyProvider = element.provider.historyProvider;
const currentHistoryItemGroup = historyProvider?.currentHistoryItemGroup;
if (!currentHistoryItemGroup || !showHistoryGraph) {
return [];
}
const historyProviderCacheEntry = this.getHistoryProviderCacheEntry(element);
let historyItemsElement = historyProviderCacheEntry.historyItems2.get(element.id);
const historyItemsMap = historyProviderCacheEntry.historyItems2;
if (!historyItemsElement) {
const historyItemGroupBase = await historyProvider.resolveHistoryItemGroupBase(currentHistoryItemGroup.id);
const historyItemGroupIds = [
currentHistoryItemGroup.id,
...currentHistoryItemGroup.base ? [currentHistoryItemGroup.base.id] : [],
...historyItemGroupBase ? [historyItemGroupBase.id] : []
];
historyItemsElement = await historyProvider.provideHistoryItems2({ historyItemGroupIds }) ?? [];
this.historyProviderCache.set(element, {
...historyProviderCacheEntry,
historyItems2: historyItemsMap.set(element.id, historyItemsElement)
});
}
return toISCMHistoryItemViewModelArray(historyItemsElement)
.map(v => ({
repository: element,
historyItemViewModel: v,
type: 'historyItem2'
}) satisfies SCMHistoryItemViewModelTreeElement);
}
private async getHistoryItemChanges(element: SCMHistoryItemTreeElement): Promise<(SCMHistoryItemChangeTreeElement | IResourceNode<SCMHistoryItemChangeTreeElement, SCMHistoryItemTreeElement>)[]> {
const repository = element.historyItemGroup.repository;
const historyProvider = repository.provider.historyProvider;
@@ -3919,13 +4093,15 @@ class SCMTreeDataSource implements IAsyncDataSource<ISCMViewService, TreeElement
showChangesSummary: boolean;
showIncomingChanges: ShowChangesSetting;
showOutgoingChanges: ShowChangesSetting;
showHistoryGraph: boolean;
} {
return {
alwaysShowRepositories: this.configurationService.getValue<boolean>('scm.alwaysShowRepositories'),
showActionButton: this.configurationService.getValue<boolean>('scm.showActionButton'),
showChangesSummary: this.configurationService.getValue<boolean>('scm.showChangesSummary'),
showIncomingChanges: this.configurationService.getValue<ShowChangesSetting>('scm.showIncomingChanges'),
showOutgoingChanges: this.configurationService.getValue<ShowChangesSetting>('scm.showOutgoingChanges')
showOutgoingChanges: this.configurationService.getValue<ShowChangesSetting>('scm.showOutgoingChanges'),
showHistoryGraph: this.configurationService.getValue<boolean>('scm.showHistoryGraph')
};
}
@@ -3963,6 +4139,7 @@ class SCMTreeDataSource implements IAsyncDataSource<ISCMViewService, TreeElement
incomingHistoryItemGroup: undefined,
outgoingHistoryItemGroup: undefined,
historyItems: new Map<string, [ISCMHistoryItem | undefined, ISCMHistoryItem[]]>(),
historyItems2: new Map<string, ISCMHistoryItem[]>(),
historyItemChanges: new Map<string, ISCMHistoryItemChange[]>()
};
}
+5 -1
View File
@@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------------*/
import * as path from 'vs/base/common/path';
import { SCMHistoryItemChangeTreeElement, SCMHistoryItemGroupTreeElement, SCMHistoryItemTreeElement, SCMViewSeparatorElement } from 'vs/workbench/contrib/scm/common/history';
import { SCMHistoryItemChangeTreeElement, SCMHistoryItemGroupTreeElement, SCMHistoryItemTreeElement, SCMHistoryItemViewModelTreeElement, SCMViewSeparatorElement } from 'vs/workbench/contrib/scm/common/history';
import { ISCMResource, ISCMRepository, ISCMResourceGroup, ISCMInput, ISCMActionButton, ISCMViewService, ISCMProvider } from 'vs/workbench/contrib/scm/common/scm';
import { IMenu, MenuItemAction } from 'vs/platform/actions/common/actions';
import { ActionBar, IActionViewItemProvider } from 'vs/base/browser/ui/actionbar/actionbar';
@@ -62,6 +62,10 @@ export function isSCMHistoryItemTreeElement(element: any): element is SCMHistory
(element as SCMHistoryItemTreeElement).type === 'historyItem';
}
export function isSCMHistoryItemViewModelTreeElement(element: any): element is SCMHistoryItemViewModelTreeElement {
return (element as SCMHistoryItemViewModelTreeElement).type === 'historyItem2';
}
export function isSCMHistoryItemChangeTreeElement(element: any): element is SCMHistoryItemChangeTreeElement {
return (element as SCMHistoryItemChangeTreeElement).type === 'historyItemChange';
}
@@ -26,8 +26,10 @@ export interface ISCMHistoryProvider {
readonly currentHistoryItemGroupObs: IObservable<ISCMHistoryItemGroup | undefined>;
provideHistoryItems(historyItemGroupId: string, options: ISCMHistoryOptions): Promise<ISCMHistoryItem[] | undefined>;
provideHistoryItems2(options: ISCMHistoryOptions): Promise<ISCMHistoryItem[] | undefined>;
provideHistoryItemSummary(historyItemId: string, historyItemParentId: string | undefined): Promise<ISCMHistoryItem | undefined>;
provideHistoryItemChanges(historyItemId: string, historyItemParentId: string | undefined): Promise<ISCMHistoryItemChange[] | undefined>;
resolveHistoryItemGroupBase(historyItemGroupId: string): Promise<ISCMHistoryItemGroup | undefined>;
resolveHistoryItemGroupCommonAncestor(historyItemGroupId1: string, historyItemGroupId2: string | undefined): Promise<{ id: string; ahead: number; behind: number } | undefined>;
}
@@ -35,12 +37,14 @@ export interface ISCMHistoryProviderCacheEntry {
readonly incomingHistoryItemGroup: SCMHistoryItemGroupTreeElement | undefined;
readonly outgoingHistoryItemGroup: SCMHistoryItemGroupTreeElement | undefined;
readonly historyItems: Map<string, [ISCMHistoryItem | undefined, ISCMHistoryItem[]]>;
readonly historyItems2: Map<string, ISCMHistoryItem[]>;
readonly historyItemChanges: Map<string, ISCMHistoryItemChange[]>;
}
export interface ISCMHistoryOptions {
readonly cursor?: string;
readonly limit?: number | { id?: string };
readonly historyItemGroupIds?: readonly string[];
}
export interface ISCMHistoryItemGroup {
@@ -68,6 +72,11 @@ export interface ISCMHistoryItemStatistics {
readonly deletions: number;
}
export interface ISCMHistoryItemLabel {
readonly title: string;
readonly icon?: URI | { light: URI; dark: URI } | ThemeIcon;
}
export interface ISCMHistoryItem {
readonly id: string;
readonly parentIds: string[];
@@ -76,6 +85,24 @@ export interface ISCMHistoryItem {
readonly icon?: URI | { light: URI; dark: URI } | ThemeIcon;
readonly timestamp?: number;
readonly statistics?: ISCMHistoryItemStatistics;
readonly labels?: ISCMHistoryItemLabel[];
}
export interface ISCMHistoryItemGraphNode {
readonly id: string;
readonly color: number;
}
export interface ISCMHistoryItemViewModel {
readonly historyItem: ISCMHistoryItem;
readonly inputSwimlanes: ISCMHistoryItemGraphNode[];
readonly outputSwimlanes: ISCMHistoryItemGraphNode[];
}
export interface SCMHistoryItemViewModelTreeElement {
readonly repository: ISCMRepository;
readonly historyItemViewModel: ISCMHistoryItemViewModel;
readonly type: 'historyItem2';
}
export interface SCMHistoryItemTreeElement extends ISCMHistoryItem {
@@ -0,0 +1,503 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as assert from 'assert';
import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils';
import { toISCMHistoryItemViewModelArray } from 'vs/workbench/contrib/scm/browser/scmHistory';
import { ISCMHistoryItem } from 'vs/workbench/contrib/scm/common/history';
suite('toISCMHistoryItemViewModelArray', () => {
ensureNoDisposablesAreLeakedInTestSuite();
test('empty graph', () => {
const viewModels = toISCMHistoryItemViewModelArray([]);
assert.strictEqual(viewModels.length, 0);
});
/**
* * a
*/
test('single commit', () => {
const models = [
{ id: 'a', parentIds: [], message: '' },
] as ISCMHistoryItem[];
const viewModels = toISCMHistoryItemViewModelArray(models);
assert.strictEqual(viewModels.length, 1);
assert.strictEqual(viewModels[0].inputSwimlanes.length, 0);
assert.strictEqual(viewModels[0].outputSwimlanes.length, 0);
});
/**
* * a(b)
* * b(c)
* * c(d)
* * d(e)
* * e
*/
test('linear graph', () => {
const models = [
{ id: 'a', parentIds: ['b'] },
{ id: 'b', parentIds: ['c'] },
{ id: 'c', parentIds: ['d'] },
{ id: 'd', parentIds: ['e'] },
{ id: 'e', parentIds: [] },
] as ISCMHistoryItem[];
const viewModels = toISCMHistoryItemViewModelArray(models);
assert.strictEqual(viewModels.length, 5);
// node a
assert.strictEqual(viewModels[0].inputSwimlanes.length, 0);
assert.strictEqual(viewModels[0].outputSwimlanes.length, 1);
assert.strictEqual(viewModels[0].outputSwimlanes[0].id, 'b');
assert.strictEqual(viewModels[0].outputSwimlanes[0].color, 0);
// node b
assert.strictEqual(viewModels[1].inputSwimlanes.length, 1);
assert.strictEqual(viewModels[1].inputSwimlanes[0].id, 'b');
assert.strictEqual(viewModels[1].inputSwimlanes[0].color, 0);
assert.strictEqual(viewModels[1].outputSwimlanes.length, 1);
assert.strictEqual(viewModels[1].outputSwimlanes[0].id, 'c');
assert.strictEqual(viewModels[1].outputSwimlanes[0].color, 0);
// node c
assert.strictEqual(viewModels[2].inputSwimlanes.length, 1);
assert.strictEqual(viewModels[2].inputSwimlanes[0].id, 'c');
assert.strictEqual(viewModels[2].inputSwimlanes[0].color, 0);
assert.strictEqual(viewModels[2].outputSwimlanes.length, 1);
assert.strictEqual(viewModels[2].outputSwimlanes[0].id, 'd');
assert.strictEqual(viewModels[2].outputSwimlanes[0].color, 0);
// node d
assert.strictEqual(viewModels[3].inputSwimlanes.length, 1);
assert.strictEqual(viewModels[3].inputSwimlanes[0].id, 'd');
assert.strictEqual(viewModels[3].inputSwimlanes[0].color, 0);
assert.strictEqual(viewModels[3].outputSwimlanes.length, 1);
assert.strictEqual(viewModels[3].outputSwimlanes[0].id, 'e');
assert.strictEqual(viewModels[3].outputSwimlanes[0].color, 0);
// node e
assert.strictEqual(viewModels[4].inputSwimlanes.length, 1);
assert.strictEqual(viewModels[4].inputSwimlanes[0].id, 'e');
assert.strictEqual(viewModels[4].inputSwimlanes[0].color, 0);
assert.strictEqual(viewModels[4].outputSwimlanes.length, 0);
});
/**
* * a(b)
* * b(c,d)
* |\
* | * d(c)
* |/
* * c(e)
* * e(f)
*/
test('merge commit (single commit in topic branch)', () => {
const models = [
{ id: 'a', parentIds: ['b'] },
{ id: 'b', parentIds: ['c', 'd'] },
{ id: 'd', parentIds: ['c'] },
{ id: 'c', parentIds: ['e'] },
{ id: 'e', parentIds: ['f'] },
] as ISCMHistoryItem[];
const viewModels = toISCMHistoryItemViewModelArray(models);
assert.strictEqual(viewModels.length, 5);
// node a
assert.strictEqual(viewModels[0].inputSwimlanes.length, 0);
assert.strictEqual(viewModels[0].outputSwimlanes.length, 1);
assert.strictEqual(viewModels[0].outputSwimlanes[0].id, 'b');
assert.strictEqual(viewModels[0].outputSwimlanes[0].color, 0);
// node b
assert.strictEqual(viewModels[1].inputSwimlanes.length, 1);
assert.strictEqual(viewModels[1].inputSwimlanes[0].id, 'b');
assert.strictEqual(viewModels[1].inputSwimlanes[0].color, 0);
assert.strictEqual(viewModels[1].outputSwimlanes.length, 2);
assert.strictEqual(viewModels[1].outputSwimlanes[0].id, 'c');
assert.strictEqual(viewModels[1].outputSwimlanes[0].color, 0);
assert.strictEqual(viewModels[1].outputSwimlanes[1].id, 'd');
assert.strictEqual(viewModels[1].outputSwimlanes[1].color, 1);
// node d
assert.strictEqual(viewModels[2].inputSwimlanes.length, 2);
assert.strictEqual(viewModels[2].inputSwimlanes[0].id, 'c');
assert.strictEqual(viewModels[2].inputSwimlanes[0].color, 0);
assert.strictEqual(viewModels[2].inputSwimlanes[1].id, 'd');
assert.strictEqual(viewModels[2].inputSwimlanes[1].color, 1);
assert.strictEqual(viewModels[2].outputSwimlanes.length, 2);
assert.strictEqual(viewModels[2].outputSwimlanes[0].id, 'c');
assert.strictEqual(viewModels[2].outputSwimlanes[0].color, 0);
assert.strictEqual(viewModels[2].outputSwimlanes[1].id, 'c');
assert.strictEqual(viewModels[2].outputSwimlanes[1].color, 1);
// node c
assert.strictEqual(viewModels[3].inputSwimlanes.length, 2);
assert.strictEqual(viewModels[3].inputSwimlanes[0].id, 'c');
assert.strictEqual(viewModels[3].inputSwimlanes[0].color, 0);
assert.strictEqual(viewModels[3].inputSwimlanes[1].id, 'c');
assert.strictEqual(viewModels[3].inputSwimlanes[1].color, 1);
assert.strictEqual(viewModels[3].outputSwimlanes.length, 1);
assert.strictEqual(viewModels[3].outputSwimlanes[0].id, 'e');
assert.strictEqual(viewModels[3].outputSwimlanes[0].color, 0);
// node e
assert.strictEqual(viewModels[4].inputSwimlanes.length, 1);
assert.strictEqual(viewModels[4].inputSwimlanes[0].id, 'e');
assert.strictEqual(viewModels[4].inputSwimlanes[0].color, 0);
assert.strictEqual(viewModels[4].outputSwimlanes.length, 1);
assert.strictEqual(viewModels[4].outputSwimlanes[0].id, 'f');
assert.strictEqual(viewModels[4].outputSwimlanes[0].color, 0);
});
/**
* * a(b,c)
* |\
* | * c(d)
* * | b(e)
* * | e(f)
* * | f(d)
* |/
* * d(g)
*/
test('merge commit (multiple commits in topic branch)', () => {
const models = [
{ id: 'a', parentIds: ['b', 'c'] },
{ id: 'c', parentIds: ['d'] },
{ id: 'b', parentIds: ['e'] },
{ id: 'e', parentIds: ['f'] },
{ id: 'f', parentIds: ['d'] },
{ id: 'd', parentIds: ['g'] },
] as ISCMHistoryItem[];
const viewModels = toISCMHistoryItemViewModelArray(models);
assert.strictEqual(viewModels.length, 6);
// node a
assert.strictEqual(viewModels[0].inputSwimlanes.length, 0);
assert.strictEqual(viewModels[0].outputSwimlanes.length, 2);
assert.strictEqual(viewModels[0].outputSwimlanes[0].id, 'b');
assert.strictEqual(viewModels[0].outputSwimlanes[0].color, 0);
assert.strictEqual(viewModels[0].outputSwimlanes[1].id, 'c');
assert.strictEqual(viewModels[0].outputSwimlanes[1].color, 1);
// node c
assert.strictEqual(viewModels[1].inputSwimlanes.length, 2);
assert.strictEqual(viewModels[1].inputSwimlanes[0].id, 'b');
assert.strictEqual(viewModels[1].inputSwimlanes[0].color, 0);
assert.strictEqual(viewModels[1].inputSwimlanes[1].id, 'c');
assert.strictEqual(viewModels[1].inputSwimlanes[1].color, 1);
assert.strictEqual(viewModels[1].outputSwimlanes.length, 2);
assert.strictEqual(viewModels[1].outputSwimlanes[0].id, 'b');
assert.strictEqual(viewModels[1].outputSwimlanes[0].color, 0);
assert.strictEqual(viewModels[1].outputSwimlanes[1].id, 'd');
assert.strictEqual(viewModels[1].outputSwimlanes[1].color, 1);
// node b
assert.strictEqual(viewModels[2].inputSwimlanes.length, 2);
assert.strictEqual(viewModels[2].inputSwimlanes[0].id, 'b');
assert.strictEqual(viewModels[2].inputSwimlanes[0].color, 0);
assert.strictEqual(viewModels[2].inputSwimlanes[1].id, 'd');
assert.strictEqual(viewModels[2].inputSwimlanes[1].color, 1);
assert.strictEqual(viewModels[2].outputSwimlanes.length, 2);
assert.strictEqual(viewModels[2].outputSwimlanes[0].id, 'e');
assert.strictEqual(viewModels[2].outputSwimlanes[0].color, 0);
assert.strictEqual(viewModels[2].outputSwimlanes[1].id, 'd');
assert.strictEqual(viewModels[2].outputSwimlanes[1].color, 1);
// node e
assert.strictEqual(viewModels[3].inputSwimlanes.length, 2);
assert.strictEqual(viewModels[3].inputSwimlanes[0].id, 'e');
assert.strictEqual(viewModels[3].inputSwimlanes[0].color, 0);
assert.strictEqual(viewModels[3].inputSwimlanes[1].id, 'd');
assert.strictEqual(viewModels[3].inputSwimlanes[1].color, 1);
assert.strictEqual(viewModels[3].outputSwimlanes.length, 2);
assert.strictEqual(viewModels[3].outputSwimlanes[0].id, 'f');
assert.strictEqual(viewModels[3].outputSwimlanes[0].color, 0);
assert.strictEqual(viewModels[3].outputSwimlanes[1].id, 'd');
assert.strictEqual(viewModels[3].outputSwimlanes[1].color, 1);
// node f
assert.strictEqual(viewModels[4].inputSwimlanes.length, 2);
assert.strictEqual(viewModels[4].inputSwimlanes[0].id, 'f');
assert.strictEqual(viewModels[4].inputSwimlanes[0].color, 0);
assert.strictEqual(viewModels[4].inputSwimlanes[1].id, 'd');
assert.strictEqual(viewModels[4].inputSwimlanes[1].color, 1);
assert.strictEqual(viewModels[4].outputSwimlanes.length, 2);
assert.strictEqual(viewModels[4].outputSwimlanes[0].id, 'd');
assert.strictEqual(viewModels[4].outputSwimlanes[0].color, 0);
assert.strictEqual(viewModels[4].outputSwimlanes[1].id, 'd');
assert.strictEqual(viewModels[4].outputSwimlanes[1].color, 1);
// node d
assert.strictEqual(viewModels[5].inputSwimlanes.length, 2);
assert.strictEqual(viewModels[5].inputSwimlanes[0].id, 'd');
assert.strictEqual(viewModels[5].inputSwimlanes[0].color, 0);
assert.strictEqual(viewModels[5].inputSwimlanes[1].id, 'd');
assert.strictEqual(viewModels[5].inputSwimlanes[1].color, 1);
assert.strictEqual(viewModels[5].outputSwimlanes.length, 1);
assert.strictEqual(viewModels[5].outputSwimlanes[0].id, 'g');
assert.strictEqual(viewModels[5].outputSwimlanes[0].color, 0);
});
/**
* * a(b,c)
* |\
* | * c(b)
* |/
* * b(d,e)
* |\
* | * e(f)
* | * f(g)
* * | d(h)
*/
test('create brach from merge commit', () => {
const models = [
{ id: 'a', parentIds: ['b', 'c'] },
{ id: 'c', parentIds: ['b'] },
{ id: 'b', parentIds: ['d', 'e'] },
{ id: 'e', parentIds: ['f'] },
{ id: 'f', parentIds: ['g'] },
{ id: 'd', parentIds: ['h'] },
] as ISCMHistoryItem[];
const viewModels = toISCMHistoryItemViewModelArray(models);
assert.strictEqual(viewModels.length, 6);
// node a
assert.strictEqual(viewModels[0].inputSwimlanes.length, 0);
assert.strictEqual(viewModels[0].outputSwimlanes.length, 2);
assert.strictEqual(viewModels[0].outputSwimlanes[0].id, 'b');
assert.strictEqual(viewModels[0].outputSwimlanes[0].color, 0);
assert.strictEqual(viewModels[0].outputSwimlanes[1].id, 'c');
assert.strictEqual(viewModels[0].outputSwimlanes[1].color, 1);
// node c
assert.strictEqual(viewModels[1].inputSwimlanes.length, 2);
assert.strictEqual(viewModels[1].inputSwimlanes[0].id, 'b');
assert.strictEqual(viewModels[1].inputSwimlanes[0].color, 0);
assert.strictEqual(viewModels[1].inputSwimlanes[1].id, 'c');
assert.strictEqual(viewModels[1].inputSwimlanes[1].color, 1);
assert.strictEqual(viewModels[1].outputSwimlanes.length, 2);
assert.strictEqual(viewModels[1].outputSwimlanes[0].id, 'b');
assert.strictEqual(viewModels[1].outputSwimlanes[0].color, 0);
assert.strictEqual(viewModels[1].outputSwimlanes[1].id, 'b');
assert.strictEqual(viewModels[1].outputSwimlanes[1].color, 1);
// node b
assert.strictEqual(viewModels[2].inputSwimlanes.length, 2);
assert.strictEqual(viewModels[2].inputSwimlanes[0].id, 'b');
assert.strictEqual(viewModels[2].inputSwimlanes[0].color, 0);
assert.strictEqual(viewModels[2].inputSwimlanes[1].id, 'b');
assert.strictEqual(viewModels[2].inputSwimlanes[1].color, 1);
assert.strictEqual(viewModels[2].outputSwimlanes.length, 2);
assert.strictEqual(viewModels[2].outputSwimlanes[0].id, 'd');
assert.strictEqual(viewModels[2].outputSwimlanes[0].color, 0);
assert.strictEqual(viewModels[2].outputSwimlanes[1].id, 'e');
assert.strictEqual(viewModels[2].outputSwimlanes[1].color, 2);
// node e
assert.strictEqual(viewModels[3].inputSwimlanes.length, 2);
assert.strictEqual(viewModels[3].inputSwimlanes[0].id, 'd');
assert.strictEqual(viewModels[3].inputSwimlanes[0].color, 0);
assert.strictEqual(viewModels[3].inputSwimlanes[1].id, 'e');
assert.strictEqual(viewModels[3].inputSwimlanes[1].color, 2);
assert.strictEqual(viewModels[3].outputSwimlanes.length, 2);
assert.strictEqual(viewModels[3].outputSwimlanes[0].id, 'd');
assert.strictEqual(viewModels[3].outputSwimlanes[0].color, 0);
assert.strictEqual(viewModels[3].outputSwimlanes[1].id, 'f');
assert.strictEqual(viewModels[3].outputSwimlanes[1].color, 2);
// node f
assert.strictEqual(viewModels[4].inputSwimlanes.length, 2);
assert.strictEqual(viewModels[4].inputSwimlanes[0].id, 'd');
assert.strictEqual(viewModels[4].inputSwimlanes[0].color, 0);
assert.strictEqual(viewModels[4].inputSwimlanes[1].id, 'f');
assert.strictEqual(viewModels[4].inputSwimlanes[1].color, 2);
assert.strictEqual(viewModels[4].outputSwimlanes.length, 2);
assert.strictEqual(viewModels[4].outputSwimlanes[0].id, 'd');
assert.strictEqual(viewModels[4].outputSwimlanes[0].color, 0);
assert.strictEqual(viewModels[4].outputSwimlanes[1].id, 'g');
assert.strictEqual(viewModels[4].outputSwimlanes[1].color, 2);
// node d
assert.strictEqual(viewModels[5].inputSwimlanes.length, 2);
assert.strictEqual(viewModels[5].inputSwimlanes[0].id, 'd');
assert.strictEqual(viewModels[5].inputSwimlanes[0].color, 0);
assert.strictEqual(viewModels[5].inputSwimlanes[1].id, 'g');
assert.strictEqual(viewModels[5].inputSwimlanes[1].color, 2);
assert.strictEqual(viewModels[5].outputSwimlanes.length, 2);
assert.strictEqual(viewModels[5].outputSwimlanes[0].id, 'h');
assert.strictEqual(viewModels[5].outputSwimlanes[0].color, 0);
assert.strictEqual(viewModels[5].outputSwimlanes[1].id, 'g');
assert.strictEqual(viewModels[5].outputSwimlanes[1].color, 2);
});
/**
* * a(b,c)
* |\
* | * c(d)
* * | b(e,f)
* |\|
* | |\
* | | * f(g)
* * | | e(g)
* | * | d(g)
* |/ /
* | /
* |/
* * g(h)
*/
test('create multiple branches from a commit', () => {
const models = [
{ id: 'a', parentIds: ['b', 'c'] },
{ id: 'c', parentIds: ['d'] },
{ id: 'b', parentIds: ['e', 'f'] },
{ id: 'f', parentIds: ['g'] },
{ id: 'e', parentIds: ['g'] },
{ id: 'd', parentIds: ['g'] },
{ id: 'g', parentIds: ['h'] },
] as ISCMHistoryItem[];
const viewModels = toISCMHistoryItemViewModelArray(models);
assert.strictEqual(viewModels.length, 7);
// node a
assert.strictEqual(viewModels[0].inputSwimlanes.length, 0);
assert.strictEqual(viewModels[0].outputSwimlanes.length, 2);
assert.strictEqual(viewModels[0].outputSwimlanes[0].id, 'b');
assert.strictEqual(viewModels[0].outputSwimlanes[0].color, 0);
assert.strictEqual(viewModels[0].outputSwimlanes[1].id, 'c');
assert.strictEqual(viewModels[0].outputSwimlanes[1].color, 1);
// node c
assert.strictEqual(viewModels[1].inputSwimlanes.length, 2);
assert.strictEqual(viewModels[1].inputSwimlanes[0].id, 'b');
assert.strictEqual(viewModels[1].inputSwimlanes[0].color, 0);
assert.strictEqual(viewModels[1].inputSwimlanes[1].id, 'c');
assert.strictEqual(viewModels[1].inputSwimlanes[1].color, 1);
assert.strictEqual(viewModels[1].outputSwimlanes.length, 2);
assert.strictEqual(viewModels[1].outputSwimlanes[0].id, 'b');
assert.strictEqual(viewModels[1].outputSwimlanes[0].color, 0);
assert.strictEqual(viewModels[1].outputSwimlanes[1].id, 'd');
assert.strictEqual(viewModels[1].outputSwimlanes[1].color, 1);
// node b
assert.strictEqual(viewModels[2].inputSwimlanes.length, 2);
assert.strictEqual(viewModels[2].inputSwimlanes[0].id, 'b');
assert.strictEqual(viewModels[2].inputSwimlanes[0].color, 0);
assert.strictEqual(viewModels[2].inputSwimlanes[1].id, 'd');
assert.strictEqual(viewModels[2].inputSwimlanes[1].color, 1);
assert.strictEqual(viewModels[2].outputSwimlanes.length, 3);
assert.strictEqual(viewModels[2].outputSwimlanes[0].id, 'e');
assert.strictEqual(viewModels[2].outputSwimlanes[0].color, 0);
assert.strictEqual(viewModels[2].outputSwimlanes[1].id, 'd');
assert.strictEqual(viewModels[2].outputSwimlanes[1].color, 1);
assert.strictEqual(viewModels[2].outputSwimlanes[2].id, 'f');
assert.strictEqual(viewModels[2].outputSwimlanes[2].color, 2);
// node f
assert.strictEqual(viewModels[3].inputSwimlanes.length, 3);
assert.strictEqual(viewModels[3].inputSwimlanes[0].id, 'e');
assert.strictEqual(viewModels[3].inputSwimlanes[0].color, 0);
assert.strictEqual(viewModels[3].inputSwimlanes[1].id, 'd');
assert.strictEqual(viewModels[3].inputSwimlanes[1].color, 1);
assert.strictEqual(viewModels[3].inputSwimlanes[2].id, 'f');
assert.strictEqual(viewModels[3].inputSwimlanes[2].color, 2);
assert.strictEqual(viewModels[3].outputSwimlanes.length, 3);
assert.strictEqual(viewModels[3].outputSwimlanes[0].id, 'e');
assert.strictEqual(viewModels[3].outputSwimlanes[0].color, 0);
assert.strictEqual(viewModels[3].outputSwimlanes[1].id, 'd');
assert.strictEqual(viewModels[3].outputSwimlanes[1].color, 1);
assert.strictEqual(viewModels[3].outputSwimlanes[2].id, 'g');
assert.strictEqual(viewModels[3].outputSwimlanes[2].color, 2);
// node e
assert.strictEqual(viewModels[4].inputSwimlanes.length, 3);
assert.strictEqual(viewModels[4].inputSwimlanes[0].id, 'e');
assert.strictEqual(viewModels[4].inputSwimlanes[0].color, 0);
assert.strictEqual(viewModels[4].inputSwimlanes[1].id, 'd');
assert.strictEqual(viewModels[4].inputSwimlanes[1].color, 1);
assert.strictEqual(viewModels[4].inputSwimlanes[2].id, 'g');
assert.strictEqual(viewModels[4].inputSwimlanes[2].color, 2);
assert.strictEqual(viewModels[4].outputSwimlanes.length, 3);
assert.strictEqual(viewModels[4].outputSwimlanes[0].id, 'g');
assert.strictEqual(viewModels[4].outputSwimlanes[0].color, 0);
assert.strictEqual(viewModels[4].outputSwimlanes[1].id, 'd');
assert.strictEqual(viewModels[4].outputSwimlanes[1].color, 1);
assert.strictEqual(viewModels[4].outputSwimlanes[2].id, 'g');
assert.strictEqual(viewModels[4].outputSwimlanes[2].color, 2);
// node d
assert.strictEqual(viewModels[5].inputSwimlanes.length, 3);
assert.strictEqual(viewModels[5].inputSwimlanes[0].id, 'g');
assert.strictEqual(viewModels[5].inputSwimlanes[0].color, 0);
assert.strictEqual(viewModels[5].inputSwimlanes[1].id, 'd');
assert.strictEqual(viewModels[5].inputSwimlanes[1].color, 1);
assert.strictEqual(viewModels[5].inputSwimlanes[2].id, 'g');
assert.strictEqual(viewModels[5].inputSwimlanes[2].color, 2);
assert.strictEqual(viewModels[5].outputSwimlanes.length, 3);
assert.strictEqual(viewModels[5].outputSwimlanes[0].id, 'g');
assert.strictEqual(viewModels[5].outputSwimlanes[0].color, 0);
assert.strictEqual(viewModels[5].outputSwimlanes[1].id, 'g');
assert.strictEqual(viewModels[5].outputSwimlanes[1].color, 1);
assert.strictEqual(viewModels[5].outputSwimlanes[2].id, 'g');
assert.strictEqual(viewModels[5].outputSwimlanes[2].color, 2);
// node g
assert.strictEqual(viewModels[6].inputSwimlanes.length, 3);
assert.strictEqual(viewModels[6].inputSwimlanes[0].id, 'g');
assert.strictEqual(viewModels[6].inputSwimlanes[0].color, 0);
assert.strictEqual(viewModels[6].inputSwimlanes[1].id, 'g');
assert.strictEqual(viewModels[6].inputSwimlanes[1].color, 1);
assert.strictEqual(viewModels[6].inputSwimlanes[2].id, 'g');
assert.strictEqual(viewModels[6].inputSwimlanes[2].color, 2);
assert.strictEqual(viewModels[6].outputSwimlanes.length, 1);
assert.strictEqual(viewModels[6].outputSwimlanes[0].id, 'h');
assert.strictEqual(viewModels[6].outputSwimlanes[0].color, 0);
});
});
+10 -6
View File
@@ -25,26 +25,24 @@ declare module 'vscode' {
// onDidChangeHistoryItemGroups: Event<SourceControlHistoryChangeEvent>;
provideHistoryItems(historyItemGroupId: string, options: SourceControlHistoryOptions, token: CancellationToken): ProviderResult<SourceControlHistoryItem[]>;
provideHistoryItems2(options: SourceControlHistoryOptions, token: CancellationToken): ProviderResult<SourceControlHistoryItem[]>;
provideHistoryItemSummary?(historyItemId: string, historyItemParentId: string | undefined, token: CancellationToken): ProviderResult<SourceControlHistoryItem>;
provideHistoryItemChanges(historyItemId: string, historyItemParentId: string | undefined, token: CancellationToken): ProviderResult<SourceControlHistoryItemChange[]>;
resolveHistoryItemGroupBase(historyItemGroupId: string, token: CancellationToken): ProviderResult<SourceControlHistoryItemGroup>;
resolveHistoryItemGroupCommonAncestor(historyItemGroupId1: string, historyItemGroupId2: string | undefined, token: CancellationToken): ProviderResult<{ id: string; ahead: number; behind: number }>;
}
export interface SourceControlHistoryOptions {
readonly cursor?: string;
readonly limit?: number | { id?: string };
readonly historyItemGroupIds?: readonly string[];
}
export interface SourceControlHistoryItemGroup {
readonly id: string;
readonly name: string;
readonly base?: Omit<SourceControlRemoteHistoryItemGroup, 'base'>;
}
export interface SourceControlRemoteHistoryItemGroup {
readonly id: string;
readonly name: string;
readonly base?: Omit<SourceControlHistoryItemGroup, 'base'>;
}
export interface SourceControlHistoryItemStatistics {
@@ -53,6 +51,11 @@ declare module 'vscode' {
readonly deletions: number;
}
export interface SourceControlHistoryItemLabel {
readonly title: string;
readonly icon?: Uri | { light: Uri; dark: Uri } | ThemeIcon;
}
export interface SourceControlHistoryItem {
readonly id: string;
readonly parentIds: string[];
@@ -61,6 +64,7 @@ declare module 'vscode' {
readonly icon?: Uri | { light: Uri; dark: Uri } | ThemeIcon;
readonly timestamp?: number;
readonly statistics?: SourceControlHistoryItemStatistics;
readonly labels?: SourceControlHistoryItemLabel[];
}
export interface SourceControlHistoryItemChange {