SCM - Initial implementation of the Sync view (#193440)

This commit is contained in:
Ladislau Szomoru 2023-09-19 15:58:22 +02:00 committed by GitHub
parent e20eb064c6
commit 1545aeab06
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 1359 additions and 128 deletions

View file

@ -18,6 +18,7 @@
"editSessionIdentityProvider",
"quickDiffProvider",
"scmActionButton",
"scmHistoryProvider",
"scmSelectedProvider",
"scmValidation",
"tabInputTextMerge",

View file

@ -20,24 +20,24 @@ interface ActionButtonState {
readonly repositoryHasChangesToCommit: boolean;
}
export class ActionButtonCommand {
private _onDidChange = new EventEmitter<void>();
abstract class AbstractActionButton {
protected _onDidChange = new EventEmitter<void>();
get onDidChange(): Event<void> { return this._onDidChange.event; }
private _state: ActionButtonState;
private get state() { return this._state; }
private set state(state: ActionButtonState) {
protected get state() { return this._state; }
protected set state(state: ActionButtonState) {
if (JSON.stringify(this._state) !== JSON.stringify(state)) {
this._state = state;
this._onDidChange.fire();
}
}
private disposables: Disposable[] = [];
abstract get button(): SourceControlActionButton | undefined;
constructor(
readonly repository: Repository,
readonly postCommitCommandCenter: CommitCommandsCenter) {
protected disposables: Disposable[] = [];
constructor(readonly repository: Repository) {
this._state = {
HEAD: undefined,
isCheckoutInProgress: false,
@ -50,6 +50,126 @@ export class ActionButtonCommand {
repository.onDidRunGitStatus(this.onDidRunGitStatus, this, this.disposables);
repository.onDidChangeOperations(this.onDidChangeOperations, this, this.disposables);
}
protected getPublishBranchActionButton(): SourceControlActionButton | undefined {
const icon = this.state.isSyncInProgress ? '$(sync~spin)' : '$(cloud-upload)';
return {
command: {
command: 'git.publish',
title: l10n.t({ message: '{0} Publish Branch', args: [icon], comment: ['{Locked="Branch"}', 'Do not translate "Branch" as it is a git term'] }),
tooltip: this.state.isSyncInProgress ?
(this.state.HEAD?.name ?
l10n.t({ message: 'Publishing Branch "{0}"...', args: [this.state.HEAD.name], comment: ['{Locked="Branch"}', 'Do not translate "Branch" as it is a git term'] }) :
l10n.t({ message: 'Publishing Branch...', comment: ['{Locked="Branch"}', 'Do not translate "Branch" as it is a git term'] })) :
(this.repository.HEAD?.name ?
l10n.t({ message: 'Publish Branch "{0}"', args: [this.state.HEAD?.name], comment: ['{Locked="Branch"}', 'Do not translate "Branch" as it is a git term'] }) :
l10n.t({ message: 'Publish Branch', comment: ['{Locked="Branch"}', 'Do not translate "Branch" as it is a git term'] })),
arguments: [this.repository.sourceControl],
},
enabled: !this.state.isCheckoutInProgress && !this.state.isSyncInProgress && !this.state.isCommitInProgress && !this.state.isMergeInProgress && !this.state.isRebaseInProgress
};
}
protected getSyncChangesActionButton(): SourceControlActionButton | undefined {
const branchIsAheadOrBehind = (this.state.HEAD?.behind ?? 0) > 0 || (this.state.HEAD?.ahead ?? 0) > 0;
const ahead = this.state.HEAD?.ahead ? ` ${this.state.HEAD.ahead}$(arrow-up)` : '';
const behind = this.state.HEAD?.behind ? ` ${this.state.HEAD.behind}$(arrow-down)` : '';
const icon = this.state.isSyncInProgress ? '$(sync~spin)' : '$(sync)';
return {
command: {
command: 'git.sync',
title: l10n.t('{0} Sync Changes{1}{2}', icon, behind, ahead),
tooltip: this.state.isSyncInProgress ?
l10n.t('Synchronizing Changes...')
: this.repository.syncTooltip,
arguments: [this.repository.sourceControl],
},
description: `${icon}${behind}${ahead}`,
enabled: !this.state.isCheckoutInProgress && !this.state.isSyncInProgress && !this.state.isCommitInProgress && !this.state.isMergeInProgress && !this.state.isRebaseInProgress && branchIsAheadOrBehind
};
}
private onDidChangeOperations(): void {
const isCheckoutInProgress
= this.repository.operations.isRunning(OperationKind.Checkout) ||
this.repository.operations.isRunning(OperationKind.CheckoutTracking);
const isCommitInProgress =
this.repository.operations.isRunning(OperationKind.Commit) ||
this.repository.operations.isRunning(OperationKind.PostCommitCommand) ||
this.repository.operations.isRunning(OperationKind.RebaseContinue);
const isSyncInProgress =
this.repository.operations.isRunning(OperationKind.Sync) ||
this.repository.operations.isRunning(OperationKind.Push) ||
this.repository.operations.isRunning(OperationKind.Pull);
this.state = { ...this.state, isCheckoutInProgress, isCommitInProgress, isSyncInProgress };
}
private onDidRunGitStatus(): void {
this.state = {
...this.state,
HEAD: this.repository.HEAD,
isMergeInProgress: this.repository.mergeGroup.resourceStates.length !== 0,
isRebaseInProgress: !!this.repository.rebaseCommit,
repositoryHasChangesToCommit: this.repositoryHasChangesToCommit()
};
}
protected repositoryHasChangesToCommit(): boolean {
const config = workspace.getConfiguration('git', Uri.file(this.repository.root));
const enableSmartCommit = config.get<boolean>('enableSmartCommit') === true;
const suggestSmartCommit = config.get<boolean>('suggestSmartCommit') === true;
const smartCommitChanges = config.get<'all' | 'tracked'>('smartCommitChanges', 'all');
const resources = [...this.repository.indexGroup.resourceStates];
if (
// Smart commit enabled (all)
(enableSmartCommit && smartCommitChanges === 'all') ||
// Smart commit disabled, smart suggestion enabled
(!enableSmartCommit && suggestSmartCommit)
) {
resources.push(...this.repository.workingTreeGroup.resourceStates);
}
// Smart commit enabled (tracked only)
if (enableSmartCommit && smartCommitChanges === 'tracked') {
resources.push(...this.repository.workingTreeGroup.resourceStates.filter(r => r.type !== Status.UNTRACKED));
}
return resources.length !== 0;
}
dispose(): void {
this.disposables = dispose(this.disposables);
}
}
export class CommitActionButton extends AbstractActionButton {
override get button(): SourceControlActionButton | undefined {
if (!this.state.HEAD) { return undefined; }
let actionButton: SourceControlActionButton | undefined;
if (this.state.repositoryHasChangesToCommit) {
// Commit Changes (enabled)
actionButton = this.getCommitActionButton();
}
// Commit Changes (enabled) -> Publish Branch -> Sync Changes -> Commit Changes (disabled)
return actionButton ?? this.getPublishBranchActionButton() ?? this.getSyncChangesActionButton() ?? this.getCommitActionButton();
}
constructor(
repository: Repository,
readonly postCommitCommandCenter: CommitCommandsCenter) {
super(repository);
this.disposables.push(repository.onDidChangeBranchProtection(() => this._onDidChange.fire()));
this.disposables.push(postCommitCommandCenter.onDidChange(() => this._onDidChange.fire()));
@ -62,7 +182,8 @@ export class ActionButtonCommand {
this.onDidChangeSmartCommitSettings();
}
if (e.affectsConfiguration('git.branchProtectionPrompt', root) ||
if (e.affectsConfiguration('scm.experimental.showSyncView') ||
e.affectsConfiguration('git.branchProtectionPrompt', root) ||
e.affectsConfiguration('git.postCommitCommand', root) ||
e.affectsConfiguration('git.rememberPostCommitCommand', root) ||
e.affectsConfiguration('git.showActionButton', root)) {
@ -71,20 +192,6 @@ export class ActionButtonCommand {
}));
}
get button(): SourceControlActionButton | undefined {
if (!this.state.HEAD) { return undefined; }
let actionButton: SourceControlActionButton | undefined;
if (this.state.repositoryHasChangesToCommit) {
// Commit Changes (enabled)
actionButton = this.getCommitActionButton();
}
// Commit Changes (enabled) -> Publish Branch -> Sync Changes -> Commit Changes (disabled)
return actionButton ?? this.getPublishBranchActionButton() ?? this.getSyncChangesActionButton() ?? this.getCommitActionButton();
}
private getCommitActionButton(): SourceControlActionButton | undefined {
const config = workspace.getConfiguration('git', Uri.file(this.repository.root));
const showActionButton = config.get<{ commit: boolean }>('showActionButton', { commit: true });
@ -133,34 +240,27 @@ export class ActionButtonCommand {
return commandGroups;
}
private getPublishBranchActionButton(): SourceControlActionButton | undefined {
protected override getPublishBranchActionButton(): SourceControlActionButton | undefined {
const scmConfig = workspace.getConfiguration('scm');
if (scmConfig.get<boolean>('experimental.showSyncView', false)) {
return undefined;
}
const config = workspace.getConfiguration('git', Uri.file(this.repository.root));
const showActionButton = config.get<{ publish: boolean }>('showActionButton', { publish: true });
// Not a branch (tag, detached), branch does have an upstream, commit/merge/rebase is in progress, or the button is disabled
if (this.state.HEAD?.type === RefType.Tag || !this.state.HEAD?.name || this.state.HEAD?.upstream || this.state.isCommitInProgress || this.state.isMergeInProgress || this.state.isRebaseInProgress || !showActionButton.publish) { return undefined; }
// Button icon
const icon = this.state.isSyncInProgress ? '$(sync~spin)' : '$(cloud-upload)';
return {
command: {
command: 'git.publish',
title: l10n.t({ message: '{0} Publish Branch', args: [icon], comment: ['{Locked="Branch"}', 'Do not translate "Branch" as it is a git term'] }),
tooltip: this.state.isSyncInProgress ?
(this.state.HEAD?.name ?
l10n.t({ message: 'Publishing Branch "{0}"...', args: [this.state.HEAD.name], comment: ['{Locked="Branch"}', 'Do not translate "Branch" as it is a git term'] }) :
l10n.t({ message: 'Publishing Branch...', comment: ['{Locked="Branch"}', 'Do not translate "Branch" as it is a git term'] })) :
(this.repository.HEAD?.name ?
l10n.t({ message: 'Publish Branch "{0}"', args: [this.state.HEAD?.name], comment: ['{Locked="Branch"}', 'Do not translate "Branch" as it is a git term'] }) :
l10n.t({ message: 'Publish Branch', comment: ['{Locked="Branch"}', 'Do not translate "Branch" as it is a git term'] })),
arguments: [this.repository.sourceControl],
},
enabled: !this.state.isCheckoutInProgress && !this.state.isSyncInProgress
};
return super.getPublishBranchActionButton();
}
private getSyncChangesActionButton(): SourceControlActionButton | undefined {
protected override getSyncChangesActionButton(): SourceControlActionButton | undefined {
const scmConfig = workspace.getConfiguration('scm');
if (scmConfig.get<boolean>('experimental.showSyncView', false)) {
return undefined;
}
const config = workspace.getConfiguration('git', Uri.file(this.repository.root));
const showActionButton = config.get<{ sync: boolean }>('showActionButton', { sync: true });
const branchIsAheadOrBehind = (this.state.HEAD?.behind ?? 0) > 0 || (this.state.HEAD?.ahead ?? 0) > 0;
@ -168,40 +268,7 @@ export class ActionButtonCommand {
// Branch does not have an upstream, branch is not ahead/behind the remote branch, commit/merge/rebase is in progress, or the button is disabled
if (!this.state.HEAD?.upstream || !branchIsAheadOrBehind || this.state.isCommitInProgress || this.state.isMergeInProgress || this.state.isRebaseInProgress || !showActionButton.sync) { return undefined; }
const ahead = this.state.HEAD.ahead ? ` ${this.state.HEAD.ahead}$(arrow-up)` : '';
const behind = this.state.HEAD.behind ? ` ${this.state.HEAD.behind}$(arrow-down)` : '';
const icon = this.state.isSyncInProgress ? '$(sync~spin)' : '$(sync)';
return {
command: {
command: 'git.sync',
title: l10n.t('{0} Sync Changes{1}{2}', icon, behind, ahead),
tooltip: this.state.isSyncInProgress ?
l10n.t('Synchronizing Changes...')
: this.repository.syncTooltip,
arguments: [this.repository.sourceControl],
},
description: `${icon}${behind}${ahead}`,
enabled: !this.state.isCheckoutInProgress && !this.state.isSyncInProgress
};
}
private onDidChangeOperations(): void {
const isCheckoutInProgress
= this.repository.operations.isRunning(OperationKind.Checkout) ||
this.repository.operations.isRunning(OperationKind.CheckoutTracking);
const isCommitInProgress =
this.repository.operations.isRunning(OperationKind.Commit) ||
this.repository.operations.isRunning(OperationKind.PostCommitCommand) ||
this.repository.operations.isRunning(OperationKind.RebaseContinue);
const isSyncInProgress =
this.repository.operations.isRunning(OperationKind.Sync) ||
this.repository.operations.isRunning(OperationKind.Push) ||
this.repository.operations.isRunning(OperationKind.Pull);
this.state = { ...this.state, isCheckoutInProgress, isCommitInProgress, isSyncInProgress };
return super.getSyncChangesActionButton();
}
private onDidChangeSmartCommitSettings(): void {
@ -210,43 +277,30 @@ export class ActionButtonCommand {
repositoryHasChangesToCommit: this.repositoryHasChangesToCommit()
};
}
}
private onDidRunGitStatus(): void {
this.state = {
...this.state,
HEAD: this.repository.HEAD,
isMergeInProgress: this.repository.mergeGroup.resourceStates.length !== 0,
isRebaseInProgress: !!this.repository.rebaseCommit,
repositoryHasChangesToCommit: this.repositoryHasChangesToCommit()
};
export class SyncActionButton extends AbstractActionButton {
override get button(): SourceControlActionButton | undefined {
if (!this.state.HEAD) { return undefined; }
// Publish Branch -> Sync Changes
return this.getPublishBranchActionButton() ?? this.getSyncChangesActionButton();
}
private repositoryHasChangesToCommit(): boolean {
const config = workspace.getConfiguration('git', Uri.file(this.repository.root));
const enableSmartCommit = config.get<boolean>('enableSmartCommit') === true;
const suggestSmartCommit = config.get<boolean>('suggestSmartCommit') === true;
const smartCommitChanges = config.get<'all' | 'tracked'>('smartCommitChanges', 'all');
constructor(repository: Repository) {
super(repository);
const resources = [...this.repository.indexGroup.resourceStates];
if (
// Smart commit enabled (all)
(enableSmartCommit && smartCommitChanges === 'all') ||
// Smart commit disabled, smart suggestion enabled
(!enableSmartCommit && suggestSmartCommit)
) {
resources.push(...this.repository.workingTreeGroup.resourceStates);
}
// Smart commit enabled (tracked only)
if (enableSmartCommit && smartCommitChanges === 'tracked') {
resources.push(...this.repository.workingTreeGroup.resourceStates.filter(r => r.type !== Status.UNTRACKED));
}
return resources.length !== 0;
this.disposables.push(workspace.onDidChangeConfiguration(e => {
if (e.affectsConfiguration('scm.experimental.showSyncView')) {
this._onDidChange.fire();
}
}));
}
dispose(): void {
this.disposables = dispose(this.disposables);
protected override getPublishBranchActionButton(): SourceControlActionButton | undefined {
// Not a branch (tag, detached), branch does have an upstream
if (this.state.HEAD?.type === RefType.Tag || this.state.HEAD?.upstream) { return undefined; }
return super.getPublishBranchActionButton();
}
}

View file

@ -131,6 +131,8 @@ export interface LogOptions {
readonly path?: string;
/** A commit range, such as "0a47c67f0fb52dd11562af48658bc1dff1d75a38..0bb4bdea78e1db44d728fd6894720071e303304f" */
readonly range?: string;
readonly reverse?: boolean;
readonly sortByAuthorDate?: boolean;
}
export interface CommitOptions {

View file

@ -1023,17 +1023,24 @@ export class Repository {
}
async log(options?: LogOptions): Promise<Commit[]> {
const maxEntries = options?.maxEntries ?? 32;
const args = ['log', `-n${maxEntries}`, `--format=${COMMIT_FORMAT}`, '-z'];
const args = ['log', `--format=${COMMIT_FORMAT}`, '-z'];
if (options?.reverse) {
args.push('--reverse', '--ancestry-path');
}
if (options?.sortByAuthorDate) {
args.push('--author-date-order');
}
if (options?.range) {
args.push(options.range);
} else {
args.push(`-n${options?.maxEntries ?? 32}`);
}
args.push('--');
if (options?.path) {
args.push(options.path);
args.push('--', options.path);
}
const result = await this.exec(args);
@ -1258,7 +1265,7 @@ export class Repository {
diffIndexWithHEAD(): Promise<Change[]>;
diffIndexWithHEAD(path: string): Promise<string>;
diffIndexWithHEAD(path?: string | undefined): Promise<string | Change[]>;
diffIndexWithHEAD(path?: string | undefined): Promise<Change[]>;
async diffIndexWithHEAD(path?: string): Promise<string | Change[]> {
if (!path) {
return await this.diffFiles(true);
@ -1303,6 +1310,17 @@ export class Repository {
return result.stdout.trim();
}
async diffBetweenShortStat(ref1: string, ref2: string): Promise<string> {
const args = ['diff', '--shortstat', `${ref1}...${ref2}`];
const result = await this.exec(args);
if (result.exitCode) {
return '';
}
return result.stdout.trim();
}
private async diffFiles(cached: boolean, ref?: string): Promise<Change[]> {
const args = ['diff', '--name-status', '-z', '--diff-filter=ADMR'];
if (cached) {
@ -2450,6 +2468,15 @@ export class Repository {
return Promise.reject<Branch>(new Error('No such branch'));
}
async getDefaultBranch(): Promise<Branch> {
const result = await this.exec(['symbolic-ref', '--short', 'refs/remotes/origin/HEAD']);
if (!result.stdout) {
throw new Error('No default branch');
}
return this.getBranch(result.stdout.trim());
}
// TODO: Support core.commentChar
stripCommitMessageComments(message: string): string {
return message.replace(/^\s*#.*$\n?/gm, '').trim();
@ -2510,6 +2537,13 @@ export class Repository {
return commits[0];
}
async getCommitCount(range: string): Promise<{ ahead: number; behind: number }> {
const result = await this.exec(['rev-list', '--count', '--left-right', range]);
const [ahead, behind] = result.stdout.trim().split('\t');
return { ahead: Number(ahead) || 0, behind: Number(behind) || 0 };
}
async updateSubmodules(paths: string[]): Promise<void> {
const args = ['submodule', 'update'];

View file

@ -0,0 +1,132 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Disposable, Event, EventEmitter, SourceControlActionButton, SourceControlHistoryItem, SourceControlHistoryItemChange, SourceControlHistoryItemGroup, SourceControlHistoryOptions, SourceControlHistoryProvider, ThemeIcon } from 'vscode';
import { Repository } from './repository';
import { IDisposable } from './util';
import { toGitUri } from './uri';
import { SyncActionButton } from './actionButton';
export class GitHistoryProvider implements SourceControlHistoryProvider, IDisposable {
private readonly _onDidChangeActionButton = new EventEmitter<void>();
readonly onDidChangeActionButton: Event<void> = this._onDidChangeActionButton.event;
private readonly _onDidChangeCurrentHistoryItemGroup = new EventEmitter<void>();
readonly onDidChangeCurrentHistoryItemGroup: Event<void> = this._onDidChangeCurrentHistoryItemGroup.event;
private _actionButton: SourceControlActionButton | undefined;
get actionButton(): SourceControlActionButton | undefined { return this._actionButton; }
set actionButton(button: SourceControlActionButton | undefined) {
this._actionButton = button;
this._onDidChangeActionButton.fire();
}
private _currentHistoryItemGroup: SourceControlHistoryItemGroup | undefined;
get currentHistoryItemGroup(): SourceControlHistoryItemGroup | undefined { return this._currentHistoryItemGroup; }
set currentHistoryItemGroup(value: SourceControlHistoryItemGroup | undefined) {
this._currentHistoryItemGroup = value;
this._onDidChangeCurrentHistoryItemGroup.fire();
}
private disposables: Disposable[] = [];
constructor(protected readonly repository: Repository) {
const actionButton = new SyncActionButton(repository);
this.actionButton = actionButton.button;
this.disposables.push(actionButton);
this.disposables.push(repository.onDidRunGitStatus(this.onDidRunGitStatus, this));
this.disposables.push(actionButton.onDidChange(() => this.actionButton = actionButton.button));
}
private async onDidRunGitStatus(): Promise<void> {
if (!this.repository.HEAD?.name || !this.repository.HEAD?.commit) { return; }
this.currentHistoryItemGroup = {
id: `refs/heads/${this.repository.HEAD.name}`,
label: this.repository.HEAD.name,
upstream: this.repository.HEAD.upstream ?
{
id: `refs/remotes/${this.repository.HEAD.upstream.remote}/${this.repository.HEAD.upstream.name}`,
label: `${this.repository.HEAD.upstream.remote}/${this.repository.HEAD.upstream.name}`,
} : undefined
};
}
async provideHistoryItems(historyItemGroupId: string, options: SourceControlHistoryOptions): Promise<SourceControlHistoryItem[]> {
//TODO@lszomoru - support limit and cursor
if (typeof options.limit === 'number') {
throw new Error('Unsupported options.');
}
if (typeof options.limit?.id !== 'string') {
throw new Error('Unsupported options.');
}
const optionsRef = options.limit.id;
const [commits, summary] = await Promise.all([
this.repository.log({ range: `${optionsRef}..${historyItemGroupId}`, sortByAuthorDate: true }),
this.getSummaryHistoryItem(optionsRef, historyItemGroupId)
]);
const historyItems = commits.length === 0 ? [] : [summary];
historyItems.push(...commits.map(commit => {
const newLineIndex = commit.message.indexOf('\n');
const subject = newLineIndex !== -1 ? commit.message.substring(0, newLineIndex) : commit.message;
return {
id: commit.hash,
parentIds: commit.parents,
label: subject,
description: commit.authorName,
icon: new ThemeIcon('account'),
timestamp: commit.authorDate?.getTime()
};
}));
return historyItems;
}
async provideHistoryItemChanges(historyItemId: string): Promise<SourceControlHistoryItemChange[]> {
const [ref1, ref2] = historyItemId.includes('..')
? historyItemId.split('..')
: [`${historyItemId}^`, historyItemId];
const changes = await this.repository.diffBetween(ref1, ref2);
return changes.map(change => ({
uri: change.uri.with({ query: `ref=${historyItemId}` }),
originalUri: toGitUri(change.originalUri, ref1),
modifiedUri: toGitUri(change.originalUri, ref2),
renameUri: change.renameUri,
}));
}
async resolveHistoryItemGroupCommonAncestor(refId1: string, refId2: string | undefined): Promise<{ id: string; ahead: number; behind: number } | undefined> {
refId2 = refId2 ?? (await this.repository.getDefaultBranch()).name ?? '';
if (refId2 === '') {
return undefined;
}
const ancestor = await this.repository.getMergeBase(refId1, refId2);
if (ancestor === '') {
return undefined;
}
const commitCount = await this.repository.getCommitCount(`${refId1}...${refId2}`);
return { id: ancestor, ahead: commitCount.ahead, behind: commitCount.behind };
}
private async getSummaryHistoryItem(ref1: string, ref2: string): Promise<SourceControlHistoryItem> {
const diffShortStat = await this.repository.diffBetweenShortStat(ref1, ref2);
return { id: `${ref1}..${ref2}`, parentIds: [], icon: new ThemeIcon('files'), label: 'Changes', description: diffShortStat };
}
dispose(): void {
this.disposables.forEach(d => d.dispose());
}
}

View file

@ -53,6 +53,7 @@ export const enum OperationKind {
RebaseContinue = 'RebaseContinue',
RevertFiles = 'RevertFiles',
RevertFilesNoProgress = 'RevertFilesNoProgress',
RevList = 'RevList',
SetBranchUpstream = 'SetBranchUpstream',
Show = 'Show',
Stage = 'Stage',
@ -69,8 +70,9 @@ export type Operation = AddOperation | ApplyOperation | BlameOperation | BranchO
GetBranchOperation | GetBranchesOperation | GetCommitTemplateOperation | GetObjectDetailsOperation | GetRefsOperation | GetRemoteRefsOperation |
HashObjectOperation | IgnoreOperation | LogOperation | LogFileOperation | MergeOperation | MergeAbortOperation | MergeBaseOperation |
MoveOperation | PostCommitCommandOperation | PullOperation | PushOperation | RemoteOperation | RenameBranchOperation | RemoveOperation |
ResetOperation | RebaseOperation | RebaseAbortOperation | RebaseContinueOperation | RevertFilesOperation | SetBranchUpstreamOperation |
ShowOperation | StageOperation | StatusOperation | StashOperation | SubmoduleUpdateOperation | SyncOperation | TagOperation;
ResetOperation | RebaseOperation | RebaseAbortOperation | RebaseContinueOperation | RevertFilesOperation | RevListOperation |
SetBranchUpstreamOperation | ShowOperation | StageOperation | StatusOperation | StashOperation | SubmoduleUpdateOperation | SyncOperation |
TagOperation;
type BaseOperation = { kind: OperationKind; blocking: boolean; readOnly: boolean; remote: boolean; retry: boolean; showProgress: boolean };
export type AddOperation = BaseOperation & { kind: OperationKind.Add };
@ -116,6 +118,7 @@ export type RebaseOperation = BaseOperation & { kind: OperationKind.Rebase };
export type RebaseAbortOperation = BaseOperation & { kind: OperationKind.RebaseAbort };
export type RebaseContinueOperation = BaseOperation & { kind: OperationKind.RebaseContinue };
export type RevertFilesOperation = BaseOperation & { kind: OperationKind.RevertFiles };
export type RevListOperation = BaseOperation & { kind: OperationKind.RevList };
export type SetBranchUpstreamOperation = BaseOperation & { kind: OperationKind.SetBranchUpstream };
export type ShowOperation = BaseOperation & { kind: OperationKind.Show };
export type StageOperation = BaseOperation & { kind: OperationKind.Stage };
@ -169,6 +172,7 @@ export const Operation = {
RebaseAbort: { kind: OperationKind.RebaseAbort, blocking: false, readOnly: false, remote: false, retry: false, showProgress: true } as RebaseAbortOperation,
RebaseContinue: { kind: OperationKind.RebaseContinue, blocking: false, readOnly: false, remote: false, retry: false, showProgress: true } as RebaseContinueOperation,
RevertFiles: (showProgress: boolean) => ({ kind: OperationKind.RevertFiles, blocking: false, readOnly: false, remote: false, retry: false, showProgress } as RevertFilesOperation),
RevList: { kind: OperationKind.RevList, blocking: false, readOnly: true, remote: false, retry: false, showProgress: false } as RevListOperation,
SetBranchUpstream: { kind: OperationKind.SetBranchUpstream, blocking: false, readOnly: false, remote: false, retry: false, showProgress: true } as SetBranchUpstreamOperation,
Show: { kind: OperationKind.Show, blocking: false, readOnly: true, remote: false, retry: false, showProgress: false } as ShowOperation,
Stage: { kind: OperationKind.Stage, blocking: false, readOnly: false, remote: false, retry: false, showProgress: true } as StageOperation,

View file

@ -19,10 +19,11 @@ import { IFileWatcher, watch } from './watch';
import { IPushErrorHandlerRegistry } from './pushError';
import { ApiRepository } from './api/api1';
import { IRemoteSourcePublisherRegistry } from './remotePublisher';
import { ActionButtonCommand } from './actionButton';
import { CommitActionButton } from './actionButton';
import { IPostCommitCommandsProviderRegistry, CommitCommandsCenter } from './postCommitCommands';
import { Operation, OperationKind, OperationManager, OperationResult } from './operation';
import { GitBranchProtectionProvider, IBranchProtectionProviderRegistry } from './branchProtection';
import { GitHistoryProvider } from './historyProvider';
const timeout = (millis: number) => new Promise(c => setTimeout(c, millis));
@ -833,8 +834,13 @@ export class Repository implements Disposable {
const root = Uri.file(repository.root);
this._sourceControl = scm.createSourceControl('git', 'Git', root);
this._sourceControl.acceptInputCommand = { command: 'git.commit', title: l10n.t('Commit'), arguments: [this._sourceControl] };
this._sourceControl.quickDiffProvider = this;
const historyProvider = new GitHistoryProvider(this);
this._sourceControl.historyProvider = historyProvider;
this.disposables.push(historyProvider);
this._sourceControl.acceptInputCommand = { command: 'git.commit', title: l10n.t('Commit'), arguments: [this._sourceControl] };
this._sourceControl.inputBox.validateInput = this.validateInput.bind(this);
this.disposables.push(this._sourceControl);
@ -921,10 +927,10 @@ export class Repository implements Disposable {
this.commitCommandCenter = new CommitCommandsCenter(globalState, this, postCommitCommandsProviderRegistry);
this.disposables.push(this.commitCommandCenter);
const actionButton = new ActionButtonCommand(this, this.commitCommandCenter);
this.disposables.push(actionButton);
actionButton.onDidChange(() => this._sourceControl.actionButton = actionButton.button);
this._sourceControl.actionButton = actionButton.button;
const commitActionButton = new CommitActionButton(this, this.commitCommandCenter);
this.disposables.push(commitActionButton);
commitActionButton.onDidChange(() => this._sourceControl.actionButton = commitActionButton.button);
this._sourceControl.actionButton = commitActionButton.button;
const progressManager = new ProgressManager(this);
this.disposables.push(progressManager);
@ -1115,6 +1121,10 @@ export class Repository implements Disposable {
return this.run(Operation.Diff, () => this.repository.diffBetween(ref1, ref2, path));
}
diffBetweenShortStat(ref1: string, ref2: string): Promise<string> {
return this.run(Operation.Diff, () => this.repository.diffBetweenShortStat(ref1, ref2));
}
getMergeBase(ref1: string, ref2: string): Promise<string> {
return this.run(Operation.MergeBase, () => this.repository.getMergeBase(ref1, ref2));
}
@ -1421,6 +1431,10 @@ export class Repository implements Disposable {
await this.run(Operation.Move, () => this.repository.move(from, to));
}
async getDefaultBranch(): Promise<Branch> {
return await this.run(Operation.GetBranch, () => this.repository.getDefaultBranch());
}
async getBranch(name: string): Promise<Branch> {
return await this.run(Operation.GetBranch, () => this.repository.getBranch(name));
}
@ -1506,6 +1520,10 @@ export class Repository implements Disposable {
return await this.repository.getCommit(ref);
}
async getCommitCount(range: string): Promise<{ ahead: number; behind: number }> {
return await this.run(Operation.RevList, () => this.repository.getCommitCount(range));
}
async reset(treeish: string, hard?: boolean): Promise<void> {
await this.run(Operation.Reset, () => this.repository.reset(treeish, hard));
}

View file

@ -12,6 +12,7 @@
"../../src/vscode-dts/vscode.d.ts",
"../../src/vscode-dts/vscode.proposed.diffCommand.d.ts",
"../../src/vscode-dts/vscode.proposed.scmActionButton.d.ts",
"../../src/vscode-dts/vscode.proposed.scmHistoryProvider.d.ts",
"../../src/vscode-dts/vscode.proposed.scmSelectedProvider.d.ts",
"../../src/vscode-dts/vscode.proposed.scmValidation.d.ts",
"../../src/vscode-dts/vscode.proposed.tabInputTextMerge.d.ts",

View file

@ -7,7 +7,7 @@ import { URI, UriComponents } from 'vs/base/common/uri';
import { Event, Emitter } from 'vs/base/common/event';
import { IDisposable, DisposableStore, combinedDisposable, dispose } 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 } from '../common/extHost.protocol';
import { ExtHostContext, MainThreadSCMShape, ExtHostSCMShape, SCMProviderFeatures, SCMRawResourceSplices, SCMGroupFeatures, MainContext, SCMHistoryItemDto, SCMActionButtonDto, SCMHistoryItemGroupDto } from '../common/extHost.protocol';
import { Command } from 'vs/editor/common/languages';
import { extHostNamedCustomer, IExtHostContext } from 'vs/workbench/services/extensions/common/extHostCustomers';
import { ISplice, Sequence } from 'vs/base/common/sequence';
@ -16,6 +16,20 @@ import { MarshalledId } from 'vs/base/common/marshallingIds';
import { ThemeIcon } from 'vs/base/common/themables';
import { IMarkdownString } from 'vs/base/common/htmlContent';
import { IQuickDiffService, QuickDiffProvider } from 'vs/workbench/contrib/scm/common/quickDiff';
import { ISCMHistoryItem, ISCMHistoryItemChange, ISCMHistoryOptions, ISCMHistoryProvider } from 'vs/workbench/contrib/scm/common/history';
function getSCMHistoryItemIcon(historyItem: SCMHistoryItemDto): URI | { light: URI; dark: URI } | ThemeIcon | undefined {
if (!historyItem.icon) {
return undefined;
} else if (URI.isUri(historyItem.icon)) {
return URI.revive(historyItem.icon);
} else if (ThemeIcon.isThemeIcon(historyItem.icon)) {
return historyItem.icon;
} else {
const icon = historyItem.icon as { light: UriComponents; dark: UriComponents };
return { light: URI.revive(icon.light), dark: URI.revive(icon.dark) };
}
}
class MainThreadSCMResourceGroup implements ISCMResourceGroup {
@ -121,6 +135,7 @@ class MainThreadSCMProvider implements ISCMProvider, QuickDiffProvider {
get contextValue(): string { return this._contextValue; }
get commitTemplate(): string { return this.features.commitTemplate || ''; }
get historyProvider(): ISCMHistoryProvider | undefined { return this._historyProvider; }
get acceptInputCommand(): Command | undefined { return this.features.acceptInputCommand; }
get actionButton(): ISCMActionButtonDescriptor | undefined { return this.features.actionButton ?? undefined; }
get statusBarCommands(): Command[] | undefined { return this.features.statusBarCommands; }
@ -132,12 +147,22 @@ class MainThreadSCMProvider implements ISCMProvider, QuickDiffProvider {
private readonly _onDidChangeStatusBarCommands = new Emitter<readonly Command[]>();
get onDidChangeStatusBarCommands(): Event<readonly Command[]> { return this._onDidChangeStatusBarCommands.event; }
private readonly _onDidChangeHistoryProviderActionButton = new Emitter<void>();
readonly onDidChangeHistoryProviderActionButton: Event<void> = this._onDidChangeHistoryProviderActionButton.event;
private readonly _onDidChangeHistoryProviderCurrentHistoryItemGroup = new Emitter<void>();
readonly onDidChangeHistoryProviderCurrentHistoryItemGroup: Event<void> = this._onDidChangeHistoryProviderCurrentHistoryItemGroup.event;
private readonly _onDidChange = new Emitter<void>();
readonly onDidChange: Event<void> = this._onDidChange.event;
private _quickDiff: IDisposable | undefined;
public readonly isSCM: boolean = true;
private _historyProvider: ISCMHistoryProvider | undefined;
private _historyProviderActionButton: SCMActionButtonDto | undefined | null;
private _historyProviderCurrentHistoryItemGroup: SCMHistoryItemGroupDto | undefined;
constructor(
private readonly proxy: ExtHostSCMShape,
private readonly _handle: number,
@ -280,6 +305,45 @@ class MainThreadSCMProvider implements ISCMProvider, QuickDiffProvider {
return result && URI.revive(result);
}
$registerHistoryProvider(): void {
this._historyProvider = {
actionButton: () => this._historyProviderActionButton ?? undefined,
currentHistoryItemGroup: () => this._historyProviderCurrentHistoryItemGroup ?? undefined,
provideHistoryItems: (historyItemGroupId, options) => this.provideHistoryItems(historyItemGroupId, options),
provideHistoryItemChanges: (historyItemId: string) => this.provideHistoryItemChanges(historyItemId),
resolveHistoryItemGroupCommonAncestor: (historyItemGroupId1, historyItemGroupId2) => this.resolveHistoryItemGroupCommonAncestor(historyItemGroupId1, historyItemGroupId2),
};
}
$onDidChangeHistoryProviderActionButton(actionButton?: SCMActionButtonDto | null): void {
this._historyProviderActionButton = actionButton;
this._onDidChangeHistoryProviderActionButton.fire();
}
$onDidChangeHistoryProviderCurrentHistoryItemGroup(currentHistoryItemGroup?: SCMHistoryItemGroupDto): void {
this._historyProviderCurrentHistoryItemGroup = currentHistoryItemGroup;
this._onDidChangeHistoryProviderCurrentHistoryItemGroup.fire();
}
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: getSCMHistoryItemIcon(historyItem), }));
}
async provideHistoryItemChanges(historyItemId: string): Promise<ISCMHistoryItemChange[] | undefined> {
const changes = await this.proxy.$provideHistoryItemChanges(this.handle, historyItemId, CancellationToken.None);
return changes?.map(change => ({
uri: URI.revive(change.uri),
originalUri: change.originalUri && URI.revive(change.originalUri),
modifiedUri: change.modifiedUri && URI.revive(change.modifiedUri),
renameUri: change.renameUri && URI.revive(change.renameUri)
}));
}
toJSON(): any {
return {
$mid: MarshalledId.ScmProvider,
@ -486,4 +550,37 @@ export class MainThreadSCM implements MainThreadSCMShape {
repository.input.validateInput = async () => undefined;
}
}
$registerHistoryProvider(sourceControlHandle: number): void {
const repository = this._repositories.get(sourceControlHandle);
if (!repository) {
return;
}
const provider = repository.provider as MainThreadSCMProvider;
provider.$registerHistoryProvider();
}
$onDidChangeHistoryProviderActionButton(sourceControlHandle: number, actionButton?: SCMActionButtonDto | null | undefined): void {
const repository = this._repositories.get(sourceControlHandle);
if (!repository) {
return;
}
const provider = repository.provider as MainThreadSCMProvider;
provider.$onDidChangeHistoryProviderActionButton(actionButton);
}
$onDidChangeHistoryProviderCurrentHistoryItemGroup(sourceControlHandle: number, historyItemGroup: SCMHistoryItemGroupDto | undefined): void {
const repository = this._repositories.get(sourceControlHandle);
if (!repository) {
return;
}
const provider = repository.provider as MainThreadSCMProvider;
provider.$onDidChangeHistoryProviderCurrentHistoryItemGroup(historyItemGroup);
}
}

View file

@ -1401,6 +1401,28 @@ export type SCMRawResourceSplices = [
SCMRawResourceSplice[]
];
export interface SCMHistoryItemGroupDto {
readonly id: string;
readonly label: string;
readonly upstream?: SCMHistoryItemGroupDto;
}
export interface SCMHistoryItemDto {
readonly id: string;
readonly parentIds: string[];
readonly label: string;
readonly description?: string;
readonly icon?: UriComponents | { light: UriComponents; dark: UriComponents } | ThemeIcon;
readonly timestamp?: number;
}
export interface SCMHistoryItemChangeDto {
readonly uri: UriComponents;
readonly originalUri: UriComponents | undefined;
readonly modifiedUri: UriComponents | undefined;
readonly renameUri: UriComponents | undefined;
}
export interface MainThreadSCMShape extends IDisposable {
$registerSourceControl(handle: number, id: string, label: string, rootUri: UriComponents | undefined, inputBoxDocumentUri: UriComponents): void;
$updateSourceControl(handle: number, features: SCMProviderFeatures): void;
@ -1419,6 +1441,10 @@ export interface MainThreadSCMShape extends IDisposable {
$setInputBoxVisibility(sourceControlHandle: number, visible: boolean): void;
$showValidationMessage(sourceControlHandle: number, message: string | IMarkdownString, type: InputValidationType): void;
$setValidationProviderIsEnabled(sourceControlHandle: number, enabled: boolean): void;
$registerHistoryProvider(sourceControlHandle: number): void;
$onDidChangeHistoryProviderActionButton(sourceControlHandle: number, actionButton?: SCMActionButtonDto | null): void;
$onDidChangeHistoryProviderCurrentHistoryItemGroup(sourceControlHandle: number, historyItemGroup: SCMHistoryItemGroupDto | undefined): void;
}
export interface MainThreadQuickDiffShape extends IDisposable {
@ -2126,6 +2152,9 @@ export interface ExtHostSCMShape {
$executeResourceCommand(sourceControlHandle: number, groupHandle: number, handle: number, preserveFocus: boolean): Promise<void>;
$validateInput(sourceControlHandle: number, value: string, cursorPosition: number): Promise<[string | IMarkdownString, number] | undefined>;
$setSelectedSourceControl(selectedSourceControlHandle: number | undefined): Promise<void>;
$provideHistoryItems(sourceControlHandle: number, historyItemGroupId: string, options: any, token: CancellationToken): Promise<SCMHistoryItemDto[] | undefined>;
$provideHistoryItemChanges(sourceControlHandle: number, historyItemId: string, token: CancellationToken): Promise<SCMHistoryItemChangeDto[] | undefined>;
$resolveHistoryItemGroupCommonAncestor(sourceControlHandle: number, historyItemGroupId1: string, historyItemGroupId2: string | undefined, token: CancellationToken): Promise<{ id: string; ahead: number; behind: number } | undefined>;
}
export interface ExtHostQuickDiffShape {

View file

@ -11,7 +11,7 @@ import { debounce } from 'vs/base/common/decorators';
import { DisposableStore, IDisposable, MutableDisposable } from 'vs/base/common/lifecycle';
import { asPromise } from 'vs/base/common/async';
import { ExtHostCommands } from 'vs/workbench/api/common/extHostCommands';
import { MainContext, MainThreadSCMShape, SCMRawResource, SCMRawResourceSplice, SCMRawResourceSplices, IMainContext, ExtHostSCMShape, ICommandDto, MainThreadTelemetryShape, SCMGroupFeatures } from './extHost.protocol';
import { MainContext, MainThreadSCMShape, SCMRawResource, SCMRawResourceSplice, SCMRawResourceSplices, IMainContext, ExtHostSCMShape, ICommandDto, MainThreadTelemetryShape, SCMGroupFeatures, SCMHistoryItemDto, SCMHistoryItemChangeDto } from './extHost.protocol';
import { sortedDiff, equals } from 'vs/base/common/arrays';
import { comparePaths } from 'vs/base/common/comparers';
import type * as vscode from 'vscode';
@ -45,6 +45,19 @@ function getIconResource(decorations?: vscode.SourceControlResourceThemableDecor
}
}
function getHistoryItemIconDto(historyItem: vscode.SourceControlHistoryItem): UriComponents | { light: UriComponents; dark: UriComponents } | ThemeIcon | undefined {
if (!historyItem.icon) {
return undefined;
} else if (URI.isUri(historyItem.icon)) {
return historyItem.icon;
} else if (ThemeIcon.isThemeIcon(historyItem.icon)) {
return historyItem.icon;
} else {
const icon = historyItem.icon as { light: URI; dark: URI };
return { light: icon.light, dark: icon.dark };
}
}
function compareResourceThemableDecorations(a: vscode.SourceControlResourceThemableDecorations, b: vscode.SourceControlResourceThemableDecorations): number {
if (!a.iconPath && !b.iconPath) {
return 0;
@ -197,6 +210,18 @@ function commandListEquals(a: readonly vscode.Command[], b: readonly vscode.Comm
return equals(a, b, commandEquals);
}
function historyItemGroupEquals(a: vscode.SourceControlHistoryItemGroup | undefined, b: vscode.SourceControlHistoryItemGroup | undefined): boolean {
if (a === b) {
return true;
}
if (!a || !b) {
return false;
}
return a.id === b.id && a.label === b.label && a.upstream?.id === b.upstream?.id && a.upstream?.label === b.upstream?.label;
}
export interface IValidateInput {
(value: string, cursorPosition: number): vscode.ProviderResult<vscode.SourceControlInputBoxValidation | undefined | null>;
}
@ -507,6 +532,52 @@ class ExtHostSourceControl implements vscode.SourceControl {
this.#proxy.$updateSourceControl(this.handle, { hasQuickDiffProvider: !!quickDiffProvider, quickDiffLabel });
}
private _historyProvider: vscode.SourceControlHistoryProvider | undefined;
private _historyProviderDisposable = new MutableDisposable<DisposableStore>();
private _historyProviderCurrentHistoryItemGroup: vscode.SourceControlHistoryItemGroup | undefined;
private _historyProviderActionButtonDisposable = new MutableDisposable<DisposableStore>();
get historyProvider(): vscode.SourceControlHistoryProvider | undefined {
checkProposedApiEnabled(this._extension, 'scmHistoryProvider');
return this._historyProvider;
}
set historyProvider(historyProvider: vscode.SourceControlHistoryProvider | undefined) {
checkProposedApiEnabled(this._extension, 'scmHistoryProvider');
this._historyProvider = historyProvider;
this._historyProviderDisposable.value = new DisposableStore();
if (!historyProvider) {
return;
}
this.#proxy.$registerHistoryProvider(this.handle);
this._historyProviderDisposable.value.add(historyProvider.onDidChangeCurrentHistoryItemGroup(() => {
if (historyItemGroupEquals(this._historyProviderCurrentHistoryItemGroup, historyProvider?.currentHistoryItemGroup)) {
return;
}
this._historyProviderCurrentHistoryItemGroup = historyProvider?.currentHistoryItemGroup;
this.#proxy.$onDidChangeHistoryProviderCurrentHistoryItemGroup(this.handle, this._historyProviderCurrentHistoryItemGroup);
}));
this._historyProviderDisposable.value.add(historyProvider.onDidChangeActionButton(() => {
checkProposedApiEnabled(this._extension, 'scmActionButton');
this._historyProviderActionButtonDisposable.value = new DisposableStore();
const internal = historyProvider.actionButton !== undefined ?
{
command: this._commands.converter.toInternal(historyProvider.actionButton.command, this._historyProviderActionButtonDisposable.value),
description: historyProvider.actionButton.description,
enabled: historyProvider.actionButton.enabled
} : undefined;
this.#proxy.$onDidChangeHistoryProviderActionButton(this.handle, internal ?? null);
}));
}
private _commitTemplate: string | undefined = undefined;
get commitTemplate(): string | undefined {
@ -882,4 +953,28 @@ export class ExtHostSCM implements ExtHostSCMShape {
this._selectedSourceControlHandle = selectedSourceControlHandle;
return Promise.resolve(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;
}
async $provideHistoryItems(sourceControlHandle: number, historyItemGroupId: string, options: any, token: CancellationToken): Promise<SCMHistoryItemDto[] | undefined> {
const historyProvider = this._sourceControls.get(sourceControlHandle)?.historyProvider;
const historyItems = await historyProvider?.provideHistoryItems(historyItemGroupId, options, token);
return historyItems?.map(item => ({
id: item.id,
parentIds: item.parentIds,
label: item.label,
description: item.description,
icon: getHistoryItemIconDto(item),
timestamp: item.timestamp,
})) ?? undefined;
}
async $provideHistoryItemChanges(sourceControlHandle: number, historyItemId: string, token: CancellationToken): Promise<SCMHistoryItemChangeDto[] | undefined> {
const historyProvider = this._sourceControls.get(sourceControlHandle)?.historyProvider;
return await historyProvider?.provideHistoryItemChanges(historyItemId, token) ?? undefined;
}
}

View file

@ -110,12 +110,50 @@
line-height: 22px;
}
.scm-view .monaco-list-row .history,
.scm-view .monaco-list-row .history-item-group,
.scm-view .monaco-list-row .resource-group {
display: flex;
height: 100%;
align-items: center;
}
.scm-view .monaco-list-row .history-item-group .monaco-highlighted-label {
display: flex;
align-items: center;
}
.scm-view .monaco-list-row .history-item-group .monaco-icon-label,
.scm-view .monaco-list-row .history-item .monaco-icon-label {
flex-grow: 1;
align-items: center;
}
.scm-view .monaco-list-row .history-item-group .monaco-icon-label > .monaco-icon-label-container {
display: flex;
}
.scm-sync-view .monaco-list-row .monaco-icon-label .icon-container
.scm-sync-view .monaco-list-row .monaco-icon-label .icon-container {
display: flex;
font-size: 14px;
padding-right: 4px;
}
.scm-sync-view .monaco-list-row .history-item .monaco-icon-label .icon-container {
display: flex;
font-size: 14px;
padding-right: 4px;
}
.scm-sync-view .monaco-list-row .history-item .monaco-icon-label .avatar {
width: 14px;
height: 14px;
border-radius: 14px;
}
.scm-view .monaco-list-row .history > .name,
.scm-view .monaco-list-row .history-item-group > .name,
.scm-view .monaco-list-row .resource-group > .name {
flex: 1;
overflow: hidden;

View file

@ -7,7 +7,7 @@ import { localize } from 'vs/nls';
import { Registry } from 'vs/platform/registry/common/platform';
import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from 'vs/workbench/common/contributions';
import { DirtyDiffWorkbenchController } from './dirtydiffDecorator';
import { VIEWLET_ID, ISCMService, VIEW_PANE_ID, ISCMProvider, ISCMViewService, REPOSITORIES_VIEW_PANE_ID } from 'vs/workbench/contrib/scm/common/scm';
import { VIEWLET_ID, ISCMService, VIEW_PANE_ID, ISCMProvider, ISCMViewService, REPOSITORIES_VIEW_PANE_ID, SYNC_VIEW_PANE_ID } from 'vs/workbench/contrib/scm/common/scm';
import { KeyMod, KeyCode } from 'vs/base/common/keyCodes';
import { MenuRegistry, MenuId } from 'vs/platform/actions/common/actions';
import { SCMActiveResourceContextKeyController, SCMStatusController } from './activity';
@ -32,6 +32,7 @@ import { Context as SuggestContext } from 'vs/editor/contrib/suggest/browser/sug
import { MANAGE_TRUST_COMMAND_ID, WorkspaceTrustContext } from 'vs/workbench/contrib/workspace/common/workspace';
import { IQuickDiffService } from 'vs/workbench/contrib/scm/common/quickDiff';
import { QuickDiffService } from 'vs/workbench/contrib/scm/common/quickDiffService';
import { SCMSyncViewPane } from 'vs/workbench/contrib/scm/browser/scmSyncViewPane';
ModesRegistry.registerLanguage({
id: 'scminput',
@ -79,7 +80,7 @@ viewsRegistry.registerViews([{
ctorDescriptor: new SyncDescriptor(SCMViewPane),
canToggleVisibility: true,
canMoveView: true,
weight: 80,
weight: 60,
order: -999,
containerIcon: sourceControlViewIcon,
openCommandActionDescriptor: {
@ -109,6 +110,17 @@ viewsRegistry.registerViews([{
containerIcon: sourceControlViewIcon
}], viewContainer);
viewsRegistry.registerViews([{
id: SYNC_VIEW_PANE_ID,
name: localize('source control sync', "Source Control Sync"),
ctorDescriptor: new SyncDescriptor(SCMSyncViewPane),
canToggleVisibility: false,
canMoveView: true,
weight: 20,
order: -998,
when: ContextKeyExpr.equals('config.scm.experimental.showSyncView', true),
}], viewContainer);
Registry.as<IWorkbenchContributionsRegistry>(WorkbenchExtensions.Workbench)
.registerWorkbenchContribution(SCMActiveResourceContextKeyController, LifecyclePhase.Restored);
@ -279,6 +291,11 @@ Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Configuration).regis
type: 'boolean',
markdownDescription: localize('showActionButton', "Controls whether an action button can be shown in the Source Control view."),
default: true
},
'scm.experimental.showSyncView': {
type: 'boolean',
description: localize('showSyncView', "Controls whether the Source Control Sync view is shown."),
default: false
}
}
});

View file

@ -0,0 +1,584 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as path from 'vs/base/common/path';
import { append, $, prepend } from 'vs/base/browser/dom';
import { CountBadge } from 'vs/base/browser/ui/countBadge/countBadge';
import { IconLabel } from 'vs/base/browser/ui/iconLabel/iconLabel';
import { IIdentityProvider, IListVirtualDelegate } from 'vs/base/browser/ui/list/list';
import { IAsyncDataSource, ITreeNode, ITreeRenderer, ITreeSorter } from 'vs/base/browser/ui/tree/tree';
import { DisposableStore, IDisposable, combinedDisposable } from 'vs/base/common/lifecycle';
import { ThemeIcon } from 'vs/base/common/themables';
import { URI } from 'vs/base/common/uri';
import { ICommandService } from 'vs/platform/commands/common/commands';
import { IConfigurationChangeEvent, IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
import { IOpenEvent, WorkbenchAsyncDataTree } from 'vs/platform/list/browser/listService';
import { IOpenerService } from 'vs/platform/opener/common/opener';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { defaultCountBadgeStyles } from 'vs/platform/theme/browser/defaultStyles';
import { IThemeService } from 'vs/platform/theme/common/themeService';
import { IResourceLabel, ResourceLabels } from 'vs/workbench/browser/labels';
import { API_OPEN_DIFF_EDITOR_COMMAND_ID } from 'vs/workbench/browser/parts/editor/editorCommands';
import { IViewPaneOptions, ViewPane } from 'vs/workbench/browser/parts/views/viewPane';
import { IViewDescriptorService } from 'vs/workbench/common/views';
import { RepositoryRenderer } from 'vs/workbench/contrib/scm/browser/scmRepositoryRenderer';
import { ActionButtonRenderer } from 'vs/workbench/contrib/scm/browser/scmViewPane';
import { getActionViewItemProvider, isSCMActionButton, isSCMRepository, isSCMRepositoryArray } from 'vs/workbench/contrib/scm/browser/util';
import { ISCMActionButton, ISCMRepository, ISCMViewService, ISCMViewVisibleRepositoryChangeEvent } from 'vs/workbench/contrib/scm/common/scm';
import { comparePaths } from 'vs/base/common/comparers';
import { ISCMHistoryItem, ISCMHistoryItemChange, ISCMHistoryItemGroup } from 'vs/workbench/contrib/scm/common/history';
import { localize } from 'vs/nls';
import { Iterable } from 'vs/base/common/iterator';
import { IListAccessibilityProvider } from 'vs/base/browser/ui/list/listWidget';
type TreeElement = ISCMRepository[] | ISCMRepository | ISCMActionButton | SCMHistoryItemGroupTreeElement | SCMHistoryItemTreeElement | SCMHistoryItemChangeTreeElement;
function isSCMHistoryItemGroupTreeElement(obj: any): obj is SCMHistoryItemGroupTreeElement {
return (obj as SCMHistoryItemGroupTreeElement).type === 'historyItemGroup';
}
function isSCMHistoryItemTreeElement(obj: any): obj is SCMHistoryItemTreeElement {
return (obj as SCMHistoryItemTreeElement).type === 'historyItem';
}
function isSCMHistoryItemChangeTreeElement(obj: any): obj is SCMHistoryItemChangeTreeElement {
return (obj as SCMHistoryItemChangeTreeElement).type === 'historyItemChange';
}
function toDiffEditorArguments(uri: URI, originalUri: URI, modifiedUri: URI): unknown[] {
const basename = path.basename(uri.fsPath);
const originalQuery = JSON.parse(originalUri.query) as { path: string; ref: string };
const modifiedQuery = JSON.parse(modifiedUri.query) as { path: string; ref: string };
const originalShortRef = originalQuery.ref.substring(0, 8).concat(originalQuery.ref.endsWith('^') ? '^' : '');
const modifiedShortRef = modifiedQuery.ref.substring(0, 8).concat(modifiedQuery.ref.endsWith('^') ? '^' : '');
return [originalUri, modifiedUri, `${basename} (${originalShortRef}) ↔ ${basename} (${modifiedShortRef})`];
}
function getSCMResourceId(element: TreeElement): string {
if (isSCMRepository(element)) {
const provider = element.provider;
return `repo:${provider.id}`;
} else if (isSCMActionButton(element)) {
const provider = element.repository.provider;
return `actionButton:${provider.id}`;
} else if (isSCMHistoryItemGroupTreeElement(element)) {
const provider = element.repository.provider;
return `historyItemGroup:${provider.id}/${element.id}`;
} else if (isSCMHistoryItemTreeElement(element)) {
const historyItemGroup = element.historyItemGroup;
const provider = historyItemGroup.repository.provider;
return `historyItem:${provider.id}/${historyItemGroup.id}/${element.id}`;
} else if (isSCMHistoryItemChangeTreeElement(element)) {
const historyItem = element.historyItem;
const historyItemGroup = historyItem.historyItemGroup;
const provider = historyItemGroup.repository.provider;
return `historyItemChange:${provider.id}/${historyItemGroup.id}/${historyItem.id}/${element.uri.toString()}`;
} else {
throw new Error('Invalid tree element');
}
}
interface SCMHistoryItemGroupTreeElement extends ISCMHistoryItemGroup {
readonly description?: string;
readonly ancestor?: string;
readonly count?: number;
readonly repository: ISCMRepository;
readonly type: 'historyItemGroup';
}
interface SCMHistoryItemTreeElement extends ISCMHistoryItem {
readonly historyItemGroup: SCMHistoryItemGroupTreeElement;
readonly type: 'historyItem';
}
interface SCMHistoryItemChangeTreeElement extends ISCMHistoryItemChange {
readonly historyItem: SCMHistoryItemTreeElement;
readonly type: 'historyItemChange';
}
class ListDelegate implements IListVirtualDelegate<any> {
getHeight(element: any): number {
if (isSCMActionButton(element)) {
return ActionButtonRenderer.DEFAULT_HEIGHT + 10;
} else {
return 22;
}
}
getTemplateId(element: any): string {
if (isSCMRepository(element)) {
return RepositoryRenderer.TEMPLATE_ID;
} else if (isSCMActionButton(element)) {
return ActionButtonRenderer.TEMPLATE_ID;
} else if (isSCMHistoryItemGroupTreeElement(element)) {
return HistoryItemGroupRenderer.TEMPLATE_ID;
} else if (isSCMHistoryItemTreeElement(element)) {
return HistoryItemRenderer.TEMPLATE_ID;
} else if (isSCMHistoryItemChangeTreeElement(element)) {
return HistoryItemChangeRenderer.TEMPLATE_ID;
} else {
throw new Error('Invalid tree element');
}
}
}
interface HistoryItemGroupTemplate {
readonly label: IconLabel;
readonly count: CountBadge;
readonly disposables: IDisposable;
}
class HistoryItemGroupRenderer implements ITreeRenderer<SCMHistoryItemGroupTreeElement, void, HistoryItemGroupTemplate> {
static readonly TEMPLATE_ID = 'history-item-group';
get templateId(): string { return HistoryItemGroupRenderer.TEMPLATE_ID; }
renderTemplate(container: HTMLElement) {
// hack
(container.parentElement!.parentElement!.querySelector('.monaco-tl-twistie')! as HTMLElement).classList.add('force-twistie');
const element = append(container, $('.history-item-group'));
const label = new IconLabel(element, { supportIcons: true });
const countContainer = append(element, $('.count'));
const count = new CountBadge(countContainer, {}, defaultCountBadgeStyles);
return { label, count, disposables: new DisposableStore() };
}
renderElement(node: ITreeNode<SCMHistoryItemGroupTreeElement>, index: number, templateData: HistoryItemGroupTemplate, height: number | undefined): void {
const historyItemGroup = node.element;
templateData.label.setLabel(historyItemGroup.label, historyItemGroup.description);
templateData.count.setCount(historyItemGroup.count ?? 0);
}
disposeTemplate(templateData: HistoryItemGroupTemplate): void {
templateData.disposables.dispose();
}
}
interface HistoryItemTemplate {
readonly iconContainer: HTMLElement;
// readonly avatarImg: HTMLImageElement;
readonly iconLabel: IconLabel;
readonly timestampContainer: HTMLElement;
readonly timestamp: HTMLSpanElement;
readonly disposables: IDisposable;
}
class HistoryItemRenderer implements ITreeRenderer<SCMHistoryItemTreeElement, void, HistoryItemTemplate> {
static readonly TEMPLATE_ID = 'history-item';
get templateId(): string { return HistoryItemRenderer.TEMPLATE_ID; }
renderTemplate(container: HTMLElement): HistoryItemTemplate {
// hack
(container.parentElement!.parentElement!.querySelector('.monaco-tl-twistie')! as HTMLElement).classList.add('force-twistie');
const element = append(container, $('.history-item'));
const iconLabel = new IconLabel(element, { supportIcons: true });
const iconContainer = prepend(iconLabel.element, $('.icon-container'));
// const avatarImg = append(iconContainer, $('img.avatar')) as HTMLImageElement;
const timestampContainer = append(iconLabel.element, $('.timestamp-container'));
const timestamp = append(timestampContainer, $('span.timestamp'));
return { iconContainer, iconLabel, timestampContainer, timestamp, disposables: new DisposableStore() };
}
renderElement(node: ITreeNode<SCMHistoryItemTreeElement, void>, index: number, templateData: HistoryItemTemplate, height: number | undefined): void {
const historyItem = node.element;
templateData.iconContainer.className = 'icon-container';
if (historyItem.icon && ThemeIcon.isThemeIcon(historyItem.icon)) {
templateData.iconContainer.classList.add(...ThemeIcon.asClassNameArray(historyItem.icon));
}
// if (commit.authorAvatar) {
// templateData.avatarImg.src = commit.authorAvatar;
// templateData.avatarImg.style.display = 'block';
// templateData.iconContainer.classList.remove(...ThemeIcon.asClassNameArray(Codicon.account));
// } else {
// templateData.avatarImg.style.display = 'none';
// templateData.iconContainer.classList.add(...ThemeIcon.asClassNameArray(Codicon.account));
// }
templateData.iconLabel.setLabel(historyItem.label, historyItem.description);
// templateData.timestampContainer.classList.toggle('timestamp-duplicate', commit.hideTimestamp === true);
// templateData.timestamp.textContent = fromNow(commit.timestamp);
}
disposeTemplate(templateData: HistoryItemTemplate): void {
templateData.disposables.dispose();
}
}
interface HistoryItemChangeTemplate {
readonly element: HTMLElement;
readonly name: HTMLElement;
readonly fileLabel: IResourceLabel;
readonly decorationIcon: HTMLElement;
readonly disposables: IDisposable;
}
class HistoryItemChangeRenderer implements ITreeRenderer<SCMHistoryItemChangeTreeElement, void, HistoryItemChangeTemplate> {
static readonly TEMPLATE_ID = 'historyItemChange';
get templateId(): string { return HistoryItemChangeRenderer.TEMPLATE_ID; }
constructor(private labels: ResourceLabels) { }
renderTemplate(container: HTMLElement): HistoryItemChangeTemplate {
const element = append(container, $('.change'));
const name = append(element, $('.name'));
const fileLabel = this.labels.create(name, { supportDescriptionHighlights: true, supportHighlights: true });
const decorationIcon = append(element, $('.decoration-icon'));
return { element, name, fileLabel, decorationIcon, disposables: new DisposableStore() };
}
renderElement(node: ITreeNode<SCMHistoryItemChangeTreeElement, void>, index: number, templateData: HistoryItemChangeTemplate, height: number | undefined): void {
templateData.fileLabel.setFile(node.element.uri, {
fileDecorations: { colors: false, badges: true },
hidePath: false,
});
}
disposeTemplate(templateData: HistoryItemChangeTemplate): void {
templateData.disposables.dispose();
}
}
class SCMSyncViewPaneAccessibilityProvider implements IListAccessibilityProvider<TreeElement> {
getAriaLabel(element: TreeElement): string {
// TODO - add aria labels
return '';
}
getWidgetAriaLabel(): string {
return localize('scmSync', 'Source Control Sync');
}
}
class SCMSyncViewPaneTreeIdentityProvider implements IIdentityProvider<TreeElement> {
getId(element: TreeElement): string {
return getSCMResourceId(element);
}
}
class SCMSyncViewPaneTreeSorter implements ITreeSorter<TreeElement> {
compare(element: TreeElement, otherElement: TreeElement): number {
// Repository
if (isSCMRepository(element)) {
if (!isSCMRepository(otherElement)) {
throw new Error('Invalid comparison');
}
return 0;
}
// Action button
if (isSCMActionButton(element)) {
return -1;
} else if (isSCMActionButton(otherElement)) {
return 1;
}
// History item group
if (isSCMHistoryItemGroupTreeElement(element)) {
if (!isSCMHistoryItemGroupTreeElement(otherElement)) {
throw new Error('Invalid comparison');
}
return 0;
}
// History item
if (isSCMHistoryItemTreeElement(element)) {
if (!isSCMHistoryItemTreeElement(otherElement)) {
throw new Error('Invalid comparison');
}
return 0;
}
// History item change
const elementPath = (element as SCMHistoryItemChangeTreeElement).uri.fsPath;
const otherElementPath = (otherElement as SCMHistoryItemChangeTreeElement).uri.fsPath;
return comparePaths(elementPath, otherElementPath);
}
}
export class SCMSyncViewPane extends ViewPane {
private listLabels!: ResourceLabels;
private _tree!: WorkbenchAsyncDataTree<TreeElement, TreeElement>;
private _viewModel!: SCMSyncPaneViewModel;
get viewModel(): SCMSyncPaneViewModel { return this._viewModel; }
private readonly disposables = new DisposableStore();
constructor(
options: IViewPaneOptions,
@ICommandService private commandService: ICommandService,
@IKeybindingService keybindingService: IKeybindingService,
@IContextMenuService contextMenuService: IContextMenuService,
@IInstantiationService instantiationService: IInstantiationService,
@IViewDescriptorService viewDescriptorService: IViewDescriptorService,
@IContextKeyService contextKeyService: IContextKeyService,
@IConfigurationService configurationService: IConfigurationService,
@IOpenerService openerService: IOpenerService,
@IThemeService themeService: IThemeService,
@ITelemetryService telemetryService: ITelemetryService
) {
super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, telemetryService);
}
protected override renderBody(container: HTMLElement): void {
super.renderBody(container);
const treeContainer = append(container, $('.scm-view.scm-sync-view.file-icon-themable-tree'));
this.listLabels = this.instantiationService.createInstance(ResourceLabels, { onDidChangeVisibility: this.onDidChangeBodyVisibility });
this._register(this.listLabels);
this._tree = this.instantiationService.createInstance(
WorkbenchAsyncDataTree,
'SCM Sync View',
treeContainer,
new ListDelegate(),
[
this.instantiationService.createInstance(RepositoryRenderer, getActionViewItemProvider(this.instantiationService)),
this.instantiationService.createInstance(ActionButtonRenderer),
this.instantiationService.createInstance(HistoryItemGroupRenderer),
this.instantiationService.createInstance(HistoryItemRenderer),
this.instantiationService.createInstance(HistoryItemChangeRenderer, this.listLabels),
],
this.instantiationService.createInstance(SCMSyncDataSource),
{
horizontalScrolling: false,
accessibilityProvider: new SCMSyncViewPaneAccessibilityProvider(),
identityProvider: new SCMSyncViewPaneTreeIdentityProvider(),
sorter: new SCMSyncViewPaneTreeSorter(),
}) as WorkbenchAsyncDataTree<TreeElement, TreeElement>;
this._register(this._tree);
this._register(this._tree.onDidOpen(this.onDidOpen, this));
this._viewModel = this.instantiationService.createInstance(SCMSyncPaneViewModel, this._tree);
}
private async onDidOpen(e: IOpenEvent<TreeElement | undefined>): Promise<void> {
if (!e.element) {
return;
} else if (isSCMHistoryItemChangeTreeElement(e.element)) {
if (e.element.originalUri && e.element.modifiedUri) {
await this.commandService.executeCommand(API_OPEN_DIFF_EDITOR_COMMAND_ID, ...toDiffEditorArguments(e.element.uri, e.element.originalUri, e.element.modifiedUri));
}
}
}
override dispose(): void {
this.disposables.dispose();
super.dispose();
}
}
class SCMSyncPaneViewModel {
private repositories = new Map<ISCMRepository, IDisposable>();
private alwaysShowRepositories = false;
private disposables = new DisposableStore();
constructor(
private readonly tree: WorkbenchAsyncDataTree<TreeElement, TreeElement>,
@ISCMViewService scmViewService: ISCMViewService,
@IConfigurationService private readonly configurationService: IConfigurationService,
) {
configurationService.onDidChangeConfiguration(this.onDidChangeConfiguration, this, this.disposables);
this.onDidChangeConfiguration();
scmViewService.onDidChangeVisibleRepositories(this._onDidChangeVisibleRepositories, this, this.disposables);
this._onDidChangeVisibleRepositories({ added: scmViewService.visibleRepositories, removed: [] });
}
private onDidChangeConfiguration(e?: IConfigurationChangeEvent): void {
if (!e || e.affectsConfiguration('scm.alwaysShowRepositories')) {
this.alwaysShowRepositories = this.configurationService.getValue<boolean>('scm.alwaysShowRepositories');
this.refresh();
}
}
private _onDidChangeVisibleRepositories({ added, removed }: ISCMViewVisibleRepositoryChangeEvent): void {
for (const repository of added) {
const repositoryDisposable: IDisposable = combinedDisposable(
repository.provider.onDidChangeHistoryProviderActionButton(() => this.refresh(repository)),
repository.provider.onDidChangeHistoryProviderCurrentHistoryItemGroup(() => this.refresh(repository))
);
this.repositories.set(repository, { dispose() { repositoryDisposable.dispose(); } });
}
for (const repository of removed) {
this.repositories.get(repository)?.dispose();
this.repositories.delete(repository);
}
this.refresh();
}
private async refresh(repository?: ISCMRepository): Promise<void> {
if (this.repositories.size === 0) {
return;
}
if (repository) {
// Particular repository
await this.tree.updateChildren(repository);
} else if (this.repositories.size === 1 && !this.alwaysShowRepositories) {
// Single repository and not always show repositories
await this.tree.setInput(Iterable.first(this.repositories.keys())!);
} else {
// Expand repository nodes
const expanded = Array.from(this.repositories.keys())
.map(repository => `repo:${repository.provider.id}`);
// Multiple repositories or always show repositories
await this.tree.setInput([...this.repositories.keys()], { expanded });
}
this.tree.layout();
}
}
class SCMSyncDataSource implements IAsyncDataSource<TreeElement, TreeElement> {
hasChildren(element: TreeElement): boolean {
if (isSCMRepositoryArray(element)) {
return true;
} else if (isSCMRepository(element)) {
return true;
} else if (isSCMActionButton(element)) {
return false;
} else if (isSCMHistoryItemGroupTreeElement(element)) {
return true;
} else if (isSCMHistoryItemTreeElement(element)) {
return true;
} else if (isSCMHistoryItemChangeTreeElement(element)) {
return false;
} else {
throw new Error('hasChildren not implemented.');
}
}
async getChildren(element: TreeElement): Promise<TreeElement[]> {
const children: TreeElement[] = [];
if (isSCMRepositoryArray(element)) {
children.push(...element);
} else if (isSCMRepository(element)) {
const scmProvider = element.provider;
const historyProvider = scmProvider.historyProvider;
const historyItemGroup = historyProvider?.currentHistoryItemGroup();
if (!historyProvider || !historyItemGroup) {
return children;
}
// Action Button
const actionButton = historyProvider.actionButton();
if (actionButton) {
children.push({
type: 'actionButton',
repository: element,
button: actionButton
} as ISCMActionButton);
}
// Common ancestor, ahead, behind
const ancestor = await historyProvider.resolveHistoryItemGroupCommonAncestor(historyItemGroup.id, historyItemGroup.upstream?.id);
// Incoming
if (historyItemGroup?.upstream) {
children.push({
id: historyItemGroup.upstream.id,
label: localize('incoming', "$(cloud-download) Incoming Changes"),
description: historyItemGroup.upstream.label,
ancestor: ancestor?.id,
count: ancestor?.behind ?? 0,
repository: element,
type: 'historyItemGroup'
} as SCMHistoryItemGroupTreeElement);
}
// Outgoing
if (historyItemGroup) {
children.push({
id: historyItemGroup.id,
label: localize('outgoing', "$(cloud-upload) Outgoing Changes"),
description: historyItemGroup.label,
ancestor: ancestor?.id,
count: ancestor?.ahead ?? 0,
repository: element,
type: 'historyItemGroup'
} as SCMHistoryItemGroupTreeElement);
}
} else if (isSCMHistoryItemGroupTreeElement(element)) {
const scmProvider = element.repository.provider;
const historyProvider = scmProvider.historyProvider;
if (!historyProvider) {
return children;
}
const historyItems = await historyProvider.provideHistoryItems(element.id, { limit: { id: element.ancestor } }) ?? [];
children.push(...historyItems.map(historyItem => ({
id: historyItem.id,
label: historyItem.label,
description: historyItem.description,
icon: historyItem.icon,
historyItemGroup: element,
type: 'historyItem'
} as SCMHistoryItemTreeElement)));
} else if (isSCMHistoryItemTreeElement(element)) {
const repository = element.historyItemGroup.repository;
const historyProvider = repository.provider.historyProvider;
if (!historyProvider) {
return children;
}
// History Item Changes
const changes = await historyProvider.provideHistoryItemChanges(element.id) ?? [];
children.push(...changes.map(change => ({
uri: change.uri,
originalUri: change.originalUri,
modifiedUri: change.modifiedUri,
renameUri: change.renameUri,
historyItem: element,
type: 'historyItemChange'
} as SCMHistoryItemChangeTreeElement)));
} else {
throw new Error('getChildren Method not implemented.');
}
return children;
}
}

View file

@ -112,7 +112,7 @@ interface ActionButtonTemplate {
readonly templateDisposable: IDisposable;
}
class ActionButtonRenderer implements ICompressibleTreeRenderer<ISCMActionButton, FuzzyScore, ActionButtonTemplate> {
export class ActionButtonRenderer implements ICompressibleTreeRenderer<ISCMActionButton, FuzzyScore, ActionButtonTemplate> {
static readonly DEFAULT_HEIGHT = 30;
static readonly TEMPLATE_ID = 'actionButton';

View file

@ -17,6 +17,10 @@ import { Command } from 'vs/editor/common/languages';
import { reset } from 'vs/base/browser/dom';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
export function isSCMRepositoryArray(element: any): element is ISCMRepository[] {
return Array.isArray(element) && element.every(r => isSCMRepository(r));
}
export function isSCMRepository(element: any): element is ISCMRepository {
return !!(element as ISCMRepository).provider && !!(element as ISCMRepository).input;
}

View file

@ -0,0 +1,43 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { ThemeIcon } from 'vs/base/common/themables';
import { URI } from 'vs/base/common/uri';
import { ISCMActionButtonDescriptor } from 'vs/workbench/contrib/scm/common/scm';
export interface ISCMHistoryProvider {
actionButton: () => ISCMActionButtonDescriptor | undefined;
currentHistoryItemGroup: () => ISCMHistoryItemGroup | undefined;
provideHistoryItems(historyItemGroupId: string, options: ISCMHistoryOptions): Promise<ISCMHistoryItem[] | undefined>;
provideHistoryItemChanges(historyItemId: string): Promise<ISCMHistoryItemChange[] | undefined>;
resolveHistoryItemGroupCommonAncestor(historyItemGroupId1: string, historyItemGroupId2: string | undefined): Promise<{ id: string; ahead: number; behind: number } | undefined>;
}
export interface ISCMHistoryOptions {
readonly cursor?: string;
readonly limit?: number | { id?: string };
}
export interface ISCMHistoryItemGroup {
readonly id: string;
readonly label: string;
readonly upstream?: ISCMHistoryItemGroup;
}
export interface ISCMHistoryItem {
readonly id: string;
readonly parentIds: string[];
readonly label: string;
readonly description?: string;
readonly icon?: URI | { light: URI; dark: URI } | ThemeIcon;
readonly timestamp?: number;
}
export interface ISCMHistoryItemChange {
readonly uri: URI;
readonly originalUri?: URI;
readonly modifiedUri?: URI;
readonly renameUri?: URI;
}

View file

@ -13,10 +13,12 @@ import { IAction } from 'vs/base/common/actions';
import { IMenu } from 'vs/platform/actions/common/actions';
import { ThemeIcon } from 'vs/base/common/themables';
import { IMarkdownString } from 'vs/base/common/htmlContent';
import { ISCMHistoryProvider } from 'vs/workbench/contrib/scm/common/history';
export const VIEWLET_ID = 'workbench.view.scm';
export const VIEW_PANE_ID = 'workbench.scm';
export const REPOSITORIES_VIEW_PANE_ID = 'workbench.scm.repositories';
export const SYNC_VIEW_PANE_ID = 'workbench.scm.sync';
export interface IBaselineResourceProvider {
getBaselineResource(resource: URI): Promise<URI>;
@ -63,7 +65,10 @@ export interface ISCMProvider extends IDisposable {
readonly inputBoxDocumentUri: URI;
readonly count?: number;
readonly commitTemplate: string;
readonly historyProvider?: ISCMHistoryProvider;
readonly onDidChangeCommitTemplate: Event<string>;
readonly onDidChangeHistoryProviderActionButton: Event<void>;
readonly onDidChangeHistoryProviderCurrentHistoryItemGroup: Event<void>;
readonly onDidChangeStatusBarCommands?: Event<readonly Command[]>;
readonly acceptInputCommand?: Command;
readonly actionButton?: ISCMActionButtonDescriptor;

View file

@ -73,6 +73,7 @@ export const allApiProposals = Object.freeze({
resolvers: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.resolvers.d.ts',
saveEditor: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.saveEditor.d.ts',
scmActionButton: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.scmActionButton.d.ts',
scmHistoryProvider: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.scmHistoryProvider.d.ts',
scmSelectedProvider: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.scmSelectedProvider.d.ts',
scmTextDocument: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.scmTextDocument.d.ts',
scmValidation: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.scmValidation.d.ts',

View file

@ -0,0 +1,72 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
declare module 'vscode' {
// https://github.com/microsoft/vscode/issues/185269
export interface SourceControl {
historyProvider?: SourceControlHistoryProvider;
}
export interface SourceControlHistoryProvider {
actionButton?: SourceControlActionButton;
currentHistoryItemGroup?: SourceControlHistoryItemGroup;
/**
* Fires when the action button changes
*/
onDidChangeActionButton: Event<void>;
/**
* Fires when the current history item group changes (ex: checkout)
*/
onDidChangeCurrentHistoryItemGroup: Event<void>;
/**
* Fires when the history item groups change (ex: commit, push, fetch)
*/
// onDidChangeHistoryItemGroups: Event<SourceControlHistoryChangeEvent>;
provideHistoryItems(historyItemGroupId: string, options: SourceControlHistoryOptions, token: CancellationToken): ProviderResult<SourceControlHistoryItem[]>;
provideHistoryItemChanges(historyItemId: string, token: CancellationToken): ProviderResult<SourceControlHistoryItemChange[]>;
resolveHistoryItemGroupCommonAncestor(historyItemGroupId1: string, historyItemGroupId2: string | undefined, token: CancellationToken): ProviderResult<{ id: string; ahead: number; behind: number }>;
// resolveHistoryItemGroup(historyItemGroupId: string, token: CancellationToken): ProviderResult<SourceControlHistoryItemGroup | undefined>;
}
export interface SourceControlHistoryOptions {
readonly cursor?: string;
readonly limit?: number | { id?: string };
}
export interface SourceControlHistoryItemGroup {
readonly id: string;
readonly label: string;
readonly upstream?: SourceControlHistoryItemGroup;
}
export interface SourceControlHistoryItem {
readonly id: string;
readonly parentIds: string[];
readonly label: string;
readonly description?: string;
readonly icon?: Uri | { light: Uri; dark: Uri } | ThemeIcon;
readonly timestamp?: number;
}
export interface SourceControlHistoryItemChange {
readonly uri: Uri;
readonly originalUri: Uri | undefined;
readonly modifiedUri: Uri | undefined;
readonly renameUri: Uri | undefined;
}
// export interface SourceControlHistoryChangeEvent {
// readonly added: Iterable<SourceControlHistoryItemGroup>;
// readonly removed: Iterable<SourceControlHistoryItemGroup>;
// readonly modified: Iterable<SourceControlHistoryItemGroup>;
// }
}