Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/git/src/actionButton.ts
3316 views
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
6
import { Command, Disposable, Event, EventEmitter, SourceControlActionButton, Uri, workspace, l10n, LogOutputChannel } from 'vscode';
7
import { Branch, RefType, Status } from './api/git';
8
import { OperationKind } from './operation';
9
import { CommitCommandsCenter } from './postCommitCommands';
10
import { Repository } from './repository';
11
import { dispose } from './util';
12
13
function isActionButtonStateEqual(state1: ActionButtonState, state2: ActionButtonState): boolean {
14
return state1.HEAD?.name === state2.HEAD?.name &&
15
state1.HEAD?.commit === state2.HEAD?.commit &&
16
state1.HEAD?.remote === state2.HEAD?.remote &&
17
state1.HEAD?.type === state2.HEAD?.type &&
18
state1.HEAD?.ahead === state2.HEAD?.ahead &&
19
state1.HEAD?.behind === state2.HEAD?.behind &&
20
state1.HEAD?.upstream?.name === state2.HEAD?.upstream?.name &&
21
state1.HEAD?.upstream?.remote === state2.HEAD?.upstream?.remote &&
22
state1.HEAD?.upstream?.commit === state2.HEAD?.upstream?.commit &&
23
state1.isCheckoutInProgress === state2.isCheckoutInProgress &&
24
state1.isCommitInProgress === state2.isCommitInProgress &&
25
state1.isMergeInProgress === state2.isMergeInProgress &&
26
state1.isRebaseInProgress === state2.isRebaseInProgress &&
27
state1.isSyncInProgress === state2.isSyncInProgress &&
28
state1.repositoryHasChangesToCommit === state2.repositoryHasChangesToCommit &&
29
state1.repositoryHasUnresolvedConflicts === state2.repositoryHasUnresolvedConflicts;
30
}
31
32
interface ActionButtonState {
33
readonly HEAD: Branch | undefined;
34
readonly isCheckoutInProgress: boolean;
35
readonly isCommitInProgress: boolean;
36
readonly isMergeInProgress: boolean;
37
readonly isRebaseInProgress: boolean;
38
readonly isSyncInProgress: boolean;
39
readonly repositoryHasChangesToCommit: boolean;
40
readonly repositoryHasUnresolvedConflicts: boolean;
41
}
42
43
export class ActionButton {
44
private _onDidChange = new EventEmitter<void>();
45
get onDidChange(): Event<void> { return this._onDidChange.event; }
46
47
private _state: ActionButtonState;
48
private get state() { return this._state; }
49
private set state(state: ActionButtonState) {
50
if (isActionButtonStateEqual(this._state, state)) {
51
return;
52
}
53
54
this.logger.trace(`[ActionButton][setState] ${JSON.stringify(state)}`);
55
56
this._state = state;
57
this._onDidChange.fire();
58
}
59
60
private disposables: Disposable[] = [];
61
62
constructor(
63
private readonly repository: Repository,
64
private readonly postCommitCommandCenter: CommitCommandsCenter,
65
private readonly logger: LogOutputChannel) {
66
this._state = {
67
HEAD: undefined,
68
isCheckoutInProgress: false,
69
isCommitInProgress: false,
70
isMergeInProgress: false,
71
isRebaseInProgress: false,
72
isSyncInProgress: false,
73
repositoryHasChangesToCommit: false,
74
repositoryHasUnresolvedConflicts: false
75
};
76
77
repository.onDidRunGitStatus(this.onDidRunGitStatus, this, this.disposables);
78
repository.onDidChangeOperations(this.onDidChangeOperations, this, this.disposables);
79
80
this.disposables.push(repository.onDidChangeBranchProtection(() => this._onDidChange.fire()));
81
this.disposables.push(postCommitCommandCenter.onDidChange(() => this._onDidChange.fire()));
82
83
const root = Uri.file(repository.root);
84
this.disposables.push(workspace.onDidChangeConfiguration(e => {
85
if (e.affectsConfiguration('git.enableSmartCommit', root) ||
86
e.affectsConfiguration('git.smartCommitChanges', root) ||
87
e.affectsConfiguration('git.suggestSmartCommit', root)) {
88
this.onDidChangeSmartCommitSettings();
89
}
90
91
if (e.affectsConfiguration('git.branchProtectionPrompt', root) ||
92
e.affectsConfiguration('git.postCommitCommand', root) ||
93
e.affectsConfiguration('git.rememberPostCommitCommand', root) ||
94
e.affectsConfiguration('git.showActionButton', root)) {
95
this._onDidChange.fire();
96
}
97
}));
98
}
99
100
get button(): SourceControlActionButton | undefined {
101
if (!this.state.HEAD) { return undefined; }
102
103
let actionButton: SourceControlActionButton | undefined;
104
105
if (this.state.repositoryHasChangesToCommit) {
106
// Commit Changes (enabled)
107
actionButton = this.getCommitActionButton();
108
}
109
110
// Commit Changes (enabled) -> Publish Branch -> Sync Changes -> Commit Changes (disabled)
111
actionButton = actionButton ?? this.getPublishBranchActionButton() ?? this.getSyncChangesActionButton() ?? this.getCommitActionButton();
112
113
this.logger.trace(`[ActionButton][getButton] ${JSON.stringify({
114
command: actionButton?.command.command,
115
title: actionButton?.command.title,
116
enabled: actionButton?.enabled
117
})}`);
118
119
return actionButton;
120
}
121
122
private getCommitActionButton(): SourceControlActionButton | undefined {
123
const config = workspace.getConfiguration('git', Uri.file(this.repository.root));
124
const showActionButton = config.get<{ commit: boolean }>('showActionButton', { commit: true });
125
126
// The button is disabled
127
if (!showActionButton.commit) { return undefined; }
128
129
const primaryCommand = this.getCommitActionButtonPrimaryCommand();
130
131
return {
132
command: primaryCommand,
133
secondaryCommands: this.getCommitActionButtonSecondaryCommands(),
134
enabled: (
135
this.state.repositoryHasChangesToCommit ||
136
(this.state.isRebaseInProgress && !this.state.repositoryHasUnresolvedConflicts) ||
137
(this.state.isMergeInProgress && !this.state.repositoryHasUnresolvedConflicts)) &&
138
!this.state.isCommitInProgress
139
};
140
}
141
142
private getCommitActionButtonPrimaryCommand(): Command {
143
// Rebase Continue
144
if (this.state.isRebaseInProgress) {
145
return {
146
command: 'git.commit',
147
title: l10n.t('{0} Continue', '$(check)'),
148
tooltip: this.state.isCommitInProgress ? l10n.t('Continuing Rebase...') : l10n.t('Continue Rebase'),
149
arguments: [this.repository.sourceControl, null]
150
};
151
}
152
153
// Merge Continue
154
if (this.state.isMergeInProgress) {
155
return {
156
command: 'git.commit',
157
title: l10n.t('{0} Continue', '$(check)'),
158
tooltip: this.state.isCommitInProgress ? l10n.t('Continuing Merge...') : l10n.t('Continue Merge'),
159
arguments: [this.repository.sourceControl, null]
160
};
161
}
162
163
// Not a branch (tag, detached)
164
if (this.state.HEAD?.type === RefType.Tag || !this.state.HEAD?.name) {
165
return {
166
command: 'git.commit',
167
title: l10n.t('{0} Commit', '$(check)'),
168
tooltip: this.state.isCommitInProgress ? l10n.t('Committing Changes...') : l10n.t('Commit Changes'),
169
arguments: [this.repository.sourceControl, null]
170
};
171
}
172
173
// Commit
174
return this.postCommitCommandCenter.getPrimaryCommand();
175
}
176
177
private getCommitActionButtonSecondaryCommands(): Command[][] {
178
// Rebase Continue
179
if (this.state.isRebaseInProgress) {
180
return [];
181
}
182
183
// Merge Continue
184
if (this.state.isMergeInProgress) {
185
return [];
186
}
187
188
// Not a branch (tag, detached)
189
if (this.state.HEAD?.type === RefType.Tag || !this.state.HEAD?.name) {
190
return [];
191
}
192
193
// Commit
194
const commandGroups: Command[][] = [];
195
for (const commands of this.postCommitCommandCenter.getSecondaryCommands()) {
196
commandGroups.push(commands.map(c => {
197
return { command: c.command, title: c.title, tooltip: c.tooltip, arguments: c.arguments };
198
}));
199
}
200
201
return commandGroups;
202
}
203
204
private getPublishBranchActionButton(): SourceControlActionButton | undefined {
205
const config = workspace.getConfiguration('git', Uri.file(this.repository.root));
206
const showActionButton = config.get<{ publish: boolean }>('showActionButton', { publish: true });
207
208
// Not a branch (tag, detached), branch does have an upstream, commit/merge/rebase is in progress, or the button is disabled
209
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; }
210
211
// Button icon
212
const icon = this.state.isSyncInProgress ? '$(sync~spin)' : '$(cloud-upload)';
213
214
return {
215
command: {
216
command: 'git.publish',
217
title: l10n.t({ message: '{0} Publish Branch', args: [icon], comment: ['{Locked="Branch"}', 'Do not translate "Branch" as it is a git term'] }),
218
tooltip: this.state.isSyncInProgress ?
219
(this.state.HEAD?.name ?
220
l10n.t({ message: 'Publishing Branch "{0}"...', args: [this.state.HEAD.name], comment: ['{Locked="Branch"}', 'Do not translate "Branch" as it is a git term'] }) :
221
l10n.t({ message: 'Publishing Branch...', comment: ['{Locked="Branch"}', 'Do not translate "Branch" as it is a git term'] })) :
222
(this.repository.HEAD?.name ?
223
l10n.t({ message: 'Publish Branch "{0}"', args: [this.state.HEAD?.name], comment: ['{Locked="Branch"}', 'Do not translate "Branch" as it is a git term'] }) :
224
l10n.t({ message: 'Publish Branch', comment: ['{Locked="Branch"}', 'Do not translate "Branch" as it is a git term'] })),
225
arguments: [this.repository.sourceControl],
226
},
227
enabled: !this.state.isCheckoutInProgress && !this.state.isSyncInProgress
228
};
229
}
230
231
private getSyncChangesActionButton(): SourceControlActionButton | undefined {
232
const config = workspace.getConfiguration('git', Uri.file(this.repository.root));
233
const showActionButton = config.get<{ sync: boolean }>('showActionButton', { sync: true });
234
const branchIsAheadOrBehind = (this.state.HEAD?.behind ?? 0) > 0 || (this.state.HEAD?.ahead ?? 0) > 0;
235
236
// 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
237
if (!this.state.HEAD?.upstream || !branchIsAheadOrBehind || this.state.isCommitInProgress || this.state.isMergeInProgress || this.state.isRebaseInProgress || !showActionButton.sync) { return undefined; }
238
239
const ahead = this.state.HEAD.ahead ? ` ${this.state.HEAD.ahead}$(arrow-up)` : '';
240
const behind = this.state.HEAD.behind ? ` ${this.state.HEAD.behind}$(arrow-down)` : '';
241
const icon = this.state.isSyncInProgress ? '$(sync~spin)' : '$(sync)';
242
243
return {
244
command: {
245
command: 'git.sync',
246
title: l10n.t('{0} Sync Changes{1}{2}', icon, behind, ahead),
247
shortTitle: `${icon}${behind}${ahead}`,
248
tooltip: this.state.isSyncInProgress ?
249
l10n.t('Synchronizing Changes...')
250
: this.repository.syncTooltip,
251
arguments: [this.repository.sourceControl],
252
},
253
enabled: !this.state.isCheckoutInProgress && !this.state.isSyncInProgress
254
};
255
}
256
257
private onDidChangeOperations(): void {
258
const isCheckoutInProgress
259
= this.repository.operations.isRunning(OperationKind.Checkout) ||
260
this.repository.operations.isRunning(OperationKind.CheckoutTracking);
261
262
const isCommitInProgress =
263
this.repository.operations.isRunning(OperationKind.Commit) ||
264
this.repository.operations.isRunning(OperationKind.PostCommitCommand) ||
265
this.repository.operations.isRunning(OperationKind.RebaseContinue);
266
267
const isSyncInProgress =
268
this.repository.operations.isRunning(OperationKind.Sync) ||
269
this.repository.operations.isRunning(OperationKind.Push) ||
270
this.repository.operations.isRunning(OperationKind.Pull);
271
272
this.state = { ...this.state, isCheckoutInProgress, isCommitInProgress, isSyncInProgress };
273
}
274
275
private onDidChangeSmartCommitSettings(): void {
276
this.state = {
277
...this.state,
278
repositoryHasChangesToCommit: this.repositoryHasChangesToCommit()
279
};
280
}
281
282
private onDidRunGitStatus(): void {
283
this.state = {
284
...this.state,
285
HEAD: this.repository.HEAD,
286
isMergeInProgress: this.repository.mergeInProgress,
287
isRebaseInProgress: !!this.repository.rebaseCommit,
288
repositoryHasChangesToCommit: this.repositoryHasChangesToCommit(),
289
repositoryHasUnresolvedConflicts: this.repository.mergeGroup.resourceStates.length > 0
290
};
291
}
292
293
private repositoryHasChangesToCommit(): boolean {
294
const config = workspace.getConfiguration('git', Uri.file(this.repository.root));
295
const enableSmartCommit = config.get<boolean>('enableSmartCommit') === true;
296
const suggestSmartCommit = config.get<boolean>('suggestSmartCommit') === true;
297
const smartCommitChanges = config.get<'all' | 'tracked'>('smartCommitChanges', 'all');
298
299
const resources = [...this.repository.indexGroup.resourceStates];
300
301
if (
302
// Smart commit enabled (all)
303
(enableSmartCommit && smartCommitChanges === 'all') ||
304
// Smart commit disabled, smart suggestion enabled
305
(!enableSmartCommit && suggestSmartCommit)
306
) {
307
resources.push(...this.repository.workingTreeGroup.resourceStates);
308
}
309
310
// Smart commit enabled (tracked only)
311
if (enableSmartCommit && smartCommitChanges === 'tracked') {
312
resources.push(...this.repository.workingTreeGroup.resourceStates.filter(r => r.type !== Status.UNTRACKED));
313
}
314
315
return resources.length !== 0;
316
}
317
318
dispose(): void {
319
this.disposables = dispose(this.disposables);
320
}
321
}
322
323