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<void>();
get onDidChange(): Event<void> { 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 '';
}
if (this.state.isCheckoutRunning) {
return '$(loading~spin)';
}
if (this.repository.HEAD.type === RefType.Head && this.repository.HEAD.name) {
return this.repository.isBranchProtected() ? '$(lock)' : '$(git-branch)';
}
if (this.repository.HEAD.type === RefType.Tag) {
return '$(tag)';
}
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<void>();
get onDidChange(): Event<void> { 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<boolean>('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<void>;
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);
}
}