mirror of
https://github.com/microsoft/vscode.git
synced 2026-05-08 09:08:48 +01:00
SCM - Initial implementation of the Sync view (#193440)
This commit is contained in:
@@ -18,6 +18,7 @@
|
||||
"editSessionIdentityProvider",
|
||||
"quickDiffProvider",
|
||||
"scmActionButton",
|
||||
"scmHistoryProvider",
|
||||
"scmSelectedProvider",
|
||||
"scmValidation",
|
||||
"tabInputTextMerge",
|
||||
|
||||
+163
-109
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
Vendored
+2
@@ -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 {
|
||||
|
||||
@@ -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'];
|
||||
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user