Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
7c43d44
Hacked up the starting point
lszomoru May 9, 2024
99331ef
Merge branch 'main' into lszomoru/scm-history-graph
lszomoru May 13, 2024
5bb7890
More progress
lszomoru May 14, 2024
d6a084d
More progress
lszomoru May 14, 2024
4c1b8b4
Simplified algo, basic support for colors
lszomoru May 15, 2024
8c3902a
Add the concept of secondary colors for merge commits
lszomoru May 15, 2024
567cafe
Further optimizations of the algo
lszomoru May 15, 2024
bf92df4
Bug fixes to clean-up algo
lszomoru May 15, 2024
f894106
Add support for curved branches
lszomoru May 15, 2024
89de101
Keep track of nodes that are moved so that the second parent for the …
lszomoru May 15, 2024
a720313
Account for offset when drawing a base commit
lszomoru May 15, 2024
690b258
Algo rework completed
lszomoru May 16, 2024
41abe80
Revert some of the hacks that were put in place to quickly get going
lszomoru May 17, 2024
391e3e4
Add caching to the history items used in the graph
lszomoru May 17, 2024
d33fd8d
Added initial tests
lszomoru May 17, 2024
f6f28b7
More work so that incoming/outgoing works along history
lszomoru May 17, 2024
00e38de
Uncomment more code
lszomoru May 17, 2024
5d42c75
Bug fixes to edge cases
lszomoru May 17, 2024
ac82315
Experiment with a new rendering for curved branches
lszomoru May 18, 2024
f0c329d
Handle repository with a single commit
lszomoru May 21, 2024
4280e5b
Merge branch 'main' into lszomoru/scm-history-graph
lszomoru May 31, 2024
0fb7352
Merge branch 'main' into lszomoru/scm-history-graph
lszomoru Jun 9, 2024
923ea31
Maintain swimlanes
lszomoru Jun 10, 2024
705a812
Fix condition
lszomoru Jun 11, 2024
7c44157
Merge branch 'main' into lszomoru/scm-incoming-outging-graph
lszomoru Jun 17, 2024
9e9b5b7
Saving my changes
lszomoru Jun 18, 2024
87918ff
More polish and clean-up
lszomoru Jun 18, 2024
352a50a
Merge branch 'main' into lszomoru/scm-incoming-outging-graph
lszomoru Jun 18, 2024
55facfc
Remove code that is not needed
lszomoru Jun 18, 2024
5539cc4
Revert change
lszomoru Jun 18, 2024
8b264cb
Revert more changes
lszomoru Jun 18, 2024
f26a0f4
More fixes
lszomoru Jun 18, 2024
d649cd2
Rename interface
lszomoru Jun 18, 2024
9ee561b
One last minor change
lszomoru Jun 18, 2024
3e26775
Pull request feedback
lszomoru Jun 19, 2024
5423207
More refactoring
lszomoru Jun 19, 2024
2fe669e
More pull request feedback
lszomoru Jun 19, 2024
2505e13
Merge branch 'main' into lszomoru/scm-incoming-outging-graph
lszomoru Jun 19, 2024
eae9011
Fix layering issues
lszomoru Jun 19, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions extensions/git/src/api/git.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@ export interface LogOptions {
readonly sortByAuthorDate?: boolean;
readonly shortStats?: boolean;
readonly author?: string;
readonly refNames?: string[];
}

export interface CommitOptions {
Expand Down
5 changes: 5 additions & 0 deletions extensions/git/src/git.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
124 changes: 119 additions & 5 deletions extensions/git/src/historyProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {

Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand All @@ -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;
}

Expand All @@ -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;
Expand Down
22 changes: 19 additions & 3 deletions src/vs/workbench/api/browser/mainThreadSCM.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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> {
Expand Down
11 changes: 11 additions & 0 deletions src/vs/workbench/api/common/extHost.protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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>;
}

Expand Down
41 changes: 30 additions & 11 deletions src/vs/workbench/api/common/extHostSCM.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { debounce } from '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';
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand All @@ -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> {
Expand All @@ -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> {
Expand Down
25 changes: 25 additions & 0 deletions src/vs/workbench/contrib/scm/browser/media/scm.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Loading