1/*---------------------------------------------------------------------------------------------
2 *  Copyright (c) Microsoft Corporation. All rights reserved.
3 *  Licensed under the MIT License. See License.txt in the project root for license information.
4 *--------------------------------------------------------------------------------------------*/
5
6import { Disposable, Command, EventEmitter, Event, workspace, Uri } from 'vscode';
7import { Repository, Operation } from './repository';
8import { anyEvent, dispose, filterEvent } from './util';
9import * as nls from 'vscode-nls';
10import { Branch } from './api/git';
11
12const localize = nls.loadMessageBundle();
13
14class CheckoutStatusBar {
15
16	private _onDidChange = new EventEmitter<void>();
17	get onDidChange(): Event<void> { return this._onDidChange.event; }
18	private disposables: Disposable[] = [];
19
20	constructor(private repository: Repository) {
21		repository.onDidRunGitStatus(this._onDidChange.fire, this._onDidChange, this.disposables);
22	}
23
24	get command(): Command | undefined {
25		const rebasing = !!this.repository.rebaseCommit;
26		const title = `$(git-branch) ${this.repository.headLabel}${rebasing ? ` (${localize('rebasing', 'Rebasing')})` : ''}`;
27
28		return {
29			command: 'git.checkout',
30			tooltip: `${this.repository.headLabel}`,
31			title,
32			arguments: [this.repository.sourceControl]
33		};
34	}
35
36	dispose(): void {
37		this.disposables.forEach(d => d.dispose());
38	}
39}
40
41interface SyncStatusBarState {
42	enabled: boolean;
43	isSyncRunning: boolean;
44	hasRemotes: boolean;
45	HEAD: Branch | undefined;
46}
47
48class SyncStatusBar {
49
50	private static StartState: SyncStatusBarState = {
51		enabled: true,
52		isSyncRunning: false,
53		hasRemotes: false,
54		HEAD: undefined
55	};
56
57	private _onDidChange = new EventEmitter<void>();
58	get onDidChange(): Event<void> { return this._onDidChange.event; }
59	private disposables: Disposable[] = [];
60
61	private _state: SyncStatusBarState = SyncStatusBar.StartState;
62	private get state() { return this._state; }
63	private set state(state: SyncStatusBarState) {
64		this._state = state;
65		this._onDidChange.fire();
66	}
67
68	constructor(private repository: Repository) {
69		repository.onDidRunGitStatus(this.onModelChange, this, this.disposables);
70		repository.onDidChangeOperations(this.onOperationsChange, this, this.disposables);
71
72		const onEnablementChange = filterEvent(workspace.onDidChangeConfiguration, e => e.affectsConfiguration('git.enableStatusBarSync'));
73		onEnablementChange(this.updateEnablement, this, this.disposables);
74
75		this._onDidChange.fire();
76	}
77
78	private updateEnablement(): void {
79		const config = workspace.getConfiguration('git', Uri.file(this.repository.root));
80		const enabled = config.get<boolean>('enableStatusBarSync', true);
81
82		this.state = { ... this.state, enabled };
83	}
84
85	private onOperationsChange(): void {
86		const isSyncRunning = this.repository.operations.isRunning(Operation.Sync) ||
87			this.repository.operations.isRunning(Operation.Push) ||
88			this.repository.operations.isRunning(Operation.Pull);
89
90		this.state = { ...this.state, isSyncRunning };
91	}
92
93	private onModelChange(): void {
94		this.state = {
95			...this.state,
96			hasRemotes: this.repository.remotes.length > 0,
97			HEAD: this.repository.HEAD
98		};
99	}
100
101	get command(): Command | undefined {
102		if (!this.state.enabled || !this.state.hasRemotes) {
103			return undefined;
104		}
105
106		const HEAD = this.state.HEAD;
107		let icon = '$(sync)';
108		let text = '';
109		let command = '';
110		let tooltip = '';
111
112		if (HEAD && HEAD.name && HEAD.commit) {
113			if (HEAD.upstream) {
114				if (HEAD.ahead || HEAD.behind) {
115					text += this.repository.syncLabel;
116				}
117
118				const config = workspace.getConfiguration('git', Uri.file(this.repository.root));
119				const rebaseWhenSync = config.get<string>('rebaseWhenSync');
120
121				command = rebaseWhenSync ? 'git.syncRebase' : 'git.sync';
122				tooltip = localize('sync changes', "Synchronize Changes");
123			} else {
124				icon = '$(cloud-upload)';
125				command = 'git.publish';
126				tooltip = localize('publish changes', "Publish Changes");
127			}
128		} else {
129			command = '';
130			tooltip = '';
131		}
132
133		if (this.state.isSyncRunning) {
134			icon = '$(sync~spin)';
135			command = '';
136			tooltip = localize('syncing changes', "Synchronizing Changes...");
137		}
138
139		return {
140			command,
141			title: [icon, text].join(' ').trim(),
142			tooltip,
143			arguments: [this.repository.sourceControl]
144		};
145	}
146
147	dispose(): void {
148		this.disposables.forEach(d => d.dispose());
149	}
150}
151
152export class StatusBarCommands {
153
154	private syncStatusBar: SyncStatusBar;
155	private checkoutStatusBar: CheckoutStatusBar;
156	private disposables: Disposable[] = [];
157
158	constructor(repository: Repository) {
159		this.syncStatusBar = new SyncStatusBar(repository);
160		this.checkoutStatusBar = new CheckoutStatusBar(repository);
161	}
162
163	get onDidChange(): Event<void> {
164		return anyEvent(
165			this.syncStatusBar.onDidChange,
166			this.checkoutStatusBar.onDidChange
167		);
168	}
169
170	get commands(): Command[] {
171		const result: Command[] = [];
172
173		const checkout = this.checkoutStatusBar.command;
174
175		if (checkout) {
176			result.push(checkout);
177		}
178
179		const sync = this.syncStatusBar.command;
180
181		if (sync) {
182			result.push(sync);
183		}
184
185		return result;
186	}
187
188	dispose(): void {
189		this.syncStatusBar.dispose();
190		this.checkoutStatusBar.dispose();
191		this.disposables = dispose(this.disposables);
192	}
193}
194