/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { Disposable, Command, EventEmitter, Event, workspace, Uri, l10n } from 'vscode'; import { Repository } from './repository'; import { anyEvent, dispose, filterEvent } from './util'; import { Branch, RefType, RemoteSourcePublisher } from './api/git'; import { IRemoteSourcePublisherRegistry } from './remotePublisher'; import { CheckoutOperation, CheckoutTrackingOperation, OperationKind } from './operation'; interface CheckoutStatusBarState { readonly isCheckoutRunning: boolean; readonly isCommitRunning: boolean; readonly isSyncRunning: boolean; } class CheckoutStatusBar { private _onDidChange = new EventEmitter(); get onDidChange(): Event { return this._onDidChange.event; } private disposables: Disposable[] = []; private _state: CheckoutStatusBarState; private get state() { return this._state; } private set state(state: CheckoutStatusBarState) { this._state = state; this._onDidChange.fire(); } constructor(private repository: Repository) { this._state = { isCheckoutRunning: false, isCommitRunning: false, isSyncRunning: false }; repository.onDidChangeOperations(this.onDidChangeOperations, this, this.disposables); repository.onDidRunGitStatus(this._onDidChange.fire, this._onDidChange, this.disposables); repository.onDidChangeBranchProtection(this._onDidChange.fire, this._onDidChange, this.disposables); } get command(): Command | undefined { const operationData = [ ...this.repository.operations.getOperations(OperationKind.Checkout) as CheckoutOperation[], ...this.repository.operations.getOperations(OperationKind.CheckoutTracking) as CheckoutTrackingOperation[] ]; const rebasing = !!this.repository.rebaseCommit; const label = operationData[0]?.refLabel ?? `${this.repository.headLabel}${rebasing ? ` (${l10n.t('Rebasing')})` : ''}`; const command = (this.state.isCheckoutRunning || this.state.isCommitRunning || this.state.isSyncRunning) ? '' : 'git.checkout'; return { command, tooltip: `${label}, ${this.getTooltip()}`, title: `${this.getIcon()} ${label}`, arguments: [this.repository.sourceControl] }; } private getIcon(): string { if (!this.repository.HEAD) { return ''; } // Checkout if (this.state.isCheckoutRunning) { return '$(loading~spin)'; } // Branch if (this.repository.HEAD.type === RefType.Head && this.repository.HEAD.name) { return this.repository.isBranchProtected() ? '$(lock)' : '$(git-branch)'; } // Tag if (this.repository.HEAD.type === RefType.Tag) { return '$(tag)'; } // Commit return '$(git-commit)'; } private getTooltip(): string { if (this.state.isCheckoutRunning) { return l10n.t('Checking Out Branch/Tag...'); } if (this.state.isCommitRunning) { return l10n.t('Committing Changes...'); } if (this.state.isSyncRunning) { return l10n.t('Synchronizing Changes...'); } return l10n.t('Checkout Branch/Tag...'); } private onDidChangeOperations(): void { const isCommitRunning = this.repository.operations.isRunning(OperationKind.Commit); const isCheckoutRunning = this.repository.operations.isRunning(OperationKind.Checkout) || this.repository.operations.isRunning(OperationKind.CheckoutTracking); const isSyncRunning = this.repository.operations.isRunning(OperationKind.Sync) || this.repository.operations.isRunning(OperationKind.Push) || this.repository.operations.isRunning(OperationKind.Pull); this.state = { ...this.state, isCheckoutRunning, isCommitRunning, isSyncRunning }; } dispose(): void { this.disposables.forEach(d => d.dispose()); } } interface SyncStatusBarState { readonly enabled: boolean; readonly isCheckoutRunning: boolean; readonly isCommitRunning: boolean; readonly isSyncRunning: boolean; readonly hasRemotes: boolean; readonly HEAD: Branch | undefined; readonly remoteSourcePublishers: RemoteSourcePublisher[]; } class SyncStatusBar { private _onDidChange = new EventEmitter(); get onDidChange(): Event { return this._onDidChange.event; } private disposables: Disposable[] = []; private _state: SyncStatusBarState; private get state() { return this._state; } private set state(state: SyncStatusBarState) { this._state = state; this._onDidChange.fire(); } constructor(private repository: Repository, private remoteSourcePublisherRegistry: IRemoteSourcePublisherRegistry) { this._state = { enabled: true, isCheckoutRunning: false, isCommitRunning: false, isSyncRunning: false, hasRemotes: false, HEAD: undefined, remoteSourcePublishers: remoteSourcePublisherRegistry.getRemoteSourcePublishers() }; repository.onDidRunGitStatus(this.onDidRunGitStatus, this, this.disposables); repository.onDidChangeOperations(this.onDidChangeOperations, this, this.disposables); anyEvent(remoteSourcePublisherRegistry.onDidAddRemoteSourcePublisher, remoteSourcePublisherRegistry.onDidRemoveRemoteSourcePublisher) (this.onDidChangeRemoteSourcePublishers, this, this.disposables); const onEnablementChange = filterEvent(workspace.onDidChangeConfiguration, e => e.affectsConfiguration('git.enableStatusBarSync')); onEnablementChange(this.updateEnablement, this, this.disposables); this.updateEnablement(); } private updateEnablement(): void { const config = workspace.getConfiguration('git', Uri.file(this.repository.root)); const enabled = config.get('enableStatusBarSync', true); this.state = { ... this.state, enabled }; } private onDidChangeOperations(): void { const isCommitRunning = this.repository.operations.isRunning(OperationKind.Commit); const isCheckoutRunning = this.repository.operations.isRunning(OperationKind.Checkout) || this.repository.operations.isRunning(OperationKind.CheckoutTracking); const isSyncRunning = this.repository.operations.isRunning(OperationKind.Sync) || this.repository.operations.isRunning(OperationKind.Push) || this.repository.operations.isRunning(OperationKind.Pull); this.state = { ...this.state, isCheckoutRunning, isCommitRunning, isSyncRunning }; } private onDidRunGitStatus(): void { this.state = { ...this.state, hasRemotes: this.repository.remotes.length > 0, HEAD: this.repository.HEAD }; } private onDidChangeRemoteSourcePublishers(): void { this.state = { ...this.state, remoteSourcePublishers: this.remoteSourcePublisherRegistry.getRemoteSourcePublishers() }; } get command(): Command | undefined { if (!this.state.enabled) { return; } if (!this.state.hasRemotes) { if (this.state.remoteSourcePublishers.length === 0) { return; } const command = (this.state.isCheckoutRunning || this.state.isCommitRunning) ? '' : 'git.publish'; const tooltip = this.state.isCheckoutRunning ? l10n.t('Checking Out Changes...') : this.state.isCommitRunning ? l10n.t('Committing Changes...') : this.state.remoteSourcePublishers.length === 1 ? l10n.t('Publish to {0}', this.state.remoteSourcePublishers[0].name) : l10n.t('Publish to...'); return { command, title: `$(cloud-upload)`, tooltip, arguments: [this.repository.sourceControl] }; } const HEAD = this.state.HEAD; let icon = '$(sync)'; let text = ''; let command = ''; let tooltip = ''; if (HEAD && HEAD.name && HEAD.commit) { if (HEAD.upstream) { if (HEAD.ahead || HEAD.behind) { text += this.repository.syncLabel; } command = 'git.sync'; tooltip = this.repository.syncTooltip; } else { icon = '$(cloud-upload)'; command = 'git.publish'; tooltip = l10n.t('Publish Branch'); } } else { command = ''; tooltip = ''; } if (this.state.isCheckoutRunning) { command = ''; tooltip = l10n.t('Checking Out Changes...'); } if (this.state.isCommitRunning) { command = ''; tooltip = l10n.t('Committing Changes...'); } if (this.state.isSyncRunning) { icon = '$(sync~spin)'; command = ''; tooltip = l10n.t('Synchronizing Changes...'); } return { command, title: [icon, text].join(' ').trim(), tooltip, arguments: [this.repository.sourceControl] }; } dispose(): void { this.disposables.forEach(d => d.dispose()); } } export class StatusBarCommands { readonly onDidChange: Event; private syncStatusBar: SyncStatusBar; private checkoutStatusBar: CheckoutStatusBar; private disposables: Disposable[] = []; constructor(repository: Repository, remoteSourcePublisherRegistry: IRemoteSourcePublisherRegistry) { this.syncStatusBar = new SyncStatusBar(repository, remoteSourcePublisherRegistry); this.checkoutStatusBar = new CheckoutStatusBar(repository); this.onDidChange = anyEvent(this.syncStatusBar.onDidChange, this.checkoutStatusBar.onDidChange); } get commands(): Command[] { return [this.checkoutStatusBar.command, this.syncStatusBar.command] .filter((c): c is Command => !!c); } dispose(): void { this.syncStatusBar.dispose(); this.checkoutStatusBar.dispose(); this.disposables = dispose(this.disposables); } }