diff --git a/extensions/git/src/api/git.d.ts b/extensions/git/src/api/git.d.ts index a9c766b95dc9e..ce27e91424441 100644 --- a/extensions/git/src/api/git.d.ts +++ b/extensions/git/src/api/git.d.ts @@ -145,6 +145,7 @@ export interface LogOptions { readonly sortByAuthorDate?: boolean; readonly shortStats?: boolean; readonly author?: string; + readonly refNames?: string[]; } export interface CommitOptions { diff --git a/extensions/git/src/git.ts b/extensions/git/src/git.ts index 46fcd6069a66a..0d27026c9ea1e 100644 --- a/extensions/git/src/git.ts +++ b/extensions/git/src/git.ts @@ -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); } diff --git a/extensions/git/src/historyProvider.ts b/extensions/git/src/historyProvider.ts index f238010e14c22..df86c284a3b1c 100644 --- a/extensions/git/src/historyProvider.ts +++ b/extensions/git/src/historyProvider.ts @@ -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 { + if (!this.currentHistoryItemGroup || !options.historyItemGroupIds) { + return []; + } + + // Deduplicate refNames + const refNames = new Set(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 { 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 { + // 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 { + private async resolveHistoryItemGroupsMergeBase(refNames: Set): Promise { + 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): 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 { 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; diff --git a/src/vs/workbench/api/browser/mainThreadSCM.ts b/src/vs/workbench/api/browser/mainThreadSCM.ts index 1aea5f1db905e..41da49088836d 100644 --- a/src/vs/workbench/api/browser/mainThreadSCM.ts +++ b/src/vs/workbench/api/browser/mainThreadSCM.ts @@ -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 { + 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 { 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 { + 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 { 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 { diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index d98887fdb239a..a8cfaa6272a70 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -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; $provideHistoryItems(sourceControlHandle: number, historyItemGroupId: string, options: any, token: CancellationToken): Promise; + $provideHistoryItems2(sourceControlHandle: number, options: any, token: CancellationToken): Promise; $provideHistoryItemSummary(sourceControlHandle: number, historyItemId: string, historyItemParentId: string | undefined, token: CancellationToken): Promise; $provideHistoryItemChanges(sourceControlHandle: number, historyItemId: string, historyItemParentId: string | undefined, token: CancellationToken): Promise; + $resolveHistoryItemGroupBase(sourceControlHandle: number, historyItemGroupId: string, token: CancellationToken): Promise; $resolveHistoryItemGroupCommonAncestor(sourceControlHandle: number, historyItemGroupId1: string, historyItemGroupId2: string | undefined, token: CancellationToken): Promise<{ id: string; ahead: number; behind: number } | undefined>; } diff --git a/src/vs/workbench/api/common/extHostSCM.ts b/src/vs/workbench/api/common/extHostSCM.ts index 14dbf15e9e203..a0284629323c7 100644 --- a/src/vs/workbench/api/common/extHostSCM.ts +++ b/src/vs/workbench/api/common/extHostSCM.ts @@ -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 { + 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 { + 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 { @@ -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 { diff --git a/src/vs/workbench/contrib/scm/browser/media/scm.css b/src/vs/workbench/contrib/scm/browser/media/scm.css index ca6716b258edf..a84c1dd22c972 100644 --- a/src/vs/workbench/contrib/scm/browser/media/scm.css +++ b/src/vs/workbench/contrib/scm/browser/media/scm.css @@ -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; diff --git a/src/vs/workbench/contrib/scm/browser/scmHistory.ts b/src/vs/workbench/contrib/scm/browser/scmHistory.ts new file mode 100644 index 0000000000000..4fe5689e8347c --- /dev/null +++ b/src/vs/workbench/contrib/scm/browser/scmHistory.ts @@ -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; +} diff --git a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts index 98090e654eea7..7102acab96e7e 100644 --- a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts +++ b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts @@ -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; // type SCMHistoryItemChangeResourceTreeNode = IResourceNode; @@ -122,6 +123,7 @@ type TreeElement = IResourceNode | SCMHistoryItemGroupTreeElement | SCMHistoryItemTreeElement | + SCMHistoryItemViewModelTreeElement | SCMHistoryItemChangeTreeElement | IResourceNode | SCMViewSeparatorElement; @@ -1003,6 +1005,104 @@ class HistoryItemRenderer implements ICompressibleTreeRenderer { + + 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, 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, 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, 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('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 { 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 { 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 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 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 { - 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 { + 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)[]> { const repository = element.historyItemGroup.repository; const historyProvider = repository.provider.historyProvider; @@ -3919,13 +4093,15 @@ class SCMTreeDataSource implements IAsyncDataSource('scm.alwaysShowRepositories'), showActionButton: this.configurationService.getValue('scm.showActionButton'), showChangesSummary: this.configurationService.getValue('scm.showChangesSummary'), showIncomingChanges: this.configurationService.getValue('scm.showIncomingChanges'), - showOutgoingChanges: this.configurationService.getValue('scm.showOutgoingChanges') + showOutgoingChanges: this.configurationService.getValue('scm.showOutgoingChanges'), + showHistoryGraph: this.configurationService.getValue('scm.showHistoryGraph') }; } @@ -3963,6 +4139,7 @@ class SCMTreeDataSource implements IAsyncDataSource(), + historyItems2: new Map(), historyItemChanges: new Map() }; } diff --git a/src/vs/workbench/contrib/scm/browser/util.ts b/src/vs/workbench/contrib/scm/browser/util.ts index 7ff931562205c..ed24dc67bf039 100644 --- a/src/vs/workbench/contrib/scm/browser/util.ts +++ b/src/vs/workbench/contrib/scm/browser/util.ts @@ -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'; } diff --git a/src/vs/workbench/contrib/scm/common/history.ts b/src/vs/workbench/contrib/scm/common/history.ts index 67c2bb3f577ed..51e4a0dead3ad 100644 --- a/src/vs/workbench/contrib/scm/common/history.ts +++ b/src/vs/workbench/contrib/scm/common/history.ts @@ -26,8 +26,10 @@ export interface ISCMHistoryProvider { readonly currentHistoryItemGroupObs: IObservable; provideHistoryItems(historyItemGroupId: string, options: ISCMHistoryOptions): Promise; + provideHistoryItems2(options: ISCMHistoryOptions): Promise; provideHistoryItemSummary(historyItemId: string, historyItemParentId: string | undefined): Promise; provideHistoryItemChanges(historyItemId: string, historyItemParentId: string | undefined): Promise; + resolveHistoryItemGroupBase(historyItemGroupId: string): Promise; 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; + readonly historyItems2: Map; readonly historyItemChanges: Map; } 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 { diff --git a/src/vs/workbench/contrib/scm/test/browser/scmHistory.test.ts b/src/vs/workbench/contrib/scm/test/browser/scmHistory.test.ts new file mode 100644 index 0000000000000..5c64ccda40249 --- /dev/null +++ b/src/vs/workbench/contrib/scm/test/browser/scmHistory.test.ts @@ -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); + }); +}); diff --git a/src/vscode-dts/vscode.proposed.scmHistoryProvider.d.ts b/src/vscode-dts/vscode.proposed.scmHistoryProvider.d.ts index e7e7ca1c85bbe..ba720bb90caa5 100644 --- a/src/vscode-dts/vscode.proposed.scmHistoryProvider.d.ts +++ b/src/vscode-dts/vscode.proposed.scmHistoryProvider.d.ts @@ -25,26 +25,24 @@ declare module 'vscode' { // onDidChangeHistoryItemGroups: Event; provideHistoryItems(historyItemGroupId: string, options: SourceControlHistoryOptions, token: CancellationToken): ProviderResult; + provideHistoryItems2(options: SourceControlHistoryOptions, token: CancellationToken): ProviderResult; provideHistoryItemSummary?(historyItemId: string, historyItemParentId: string | undefined, token: CancellationToken): ProviderResult; provideHistoryItemChanges(historyItemId: string, historyItemParentId: string | undefined, token: CancellationToken): ProviderResult; + resolveHistoryItemGroupBase(historyItemGroupId: string, token: CancellationToken): ProviderResult; 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; - } - - export interface SourceControlRemoteHistoryItemGroup { - readonly id: string; - readonly name: string; + readonly base?: Omit; } 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 {