diff --git a/extensions/git/package.json b/extensions/git/package.json index f44453c91c1..08ef635454d 100644 --- a/extensions/git/package.json +++ b/extensions/git/package.json @@ -2192,6 +2192,12 @@ "scope": "resource", "default": "none" }, + "git.rememberPostCommitCommand": { + "type": "boolean", + "description": "%config.rememberPostCommitCommand%", + "scope": "resource", + "default": false + }, "git.openAfterClone": { "type": "string", "enum": [ diff --git a/extensions/git/package.nls.json b/extensions/git/package.nls.json index 5fc0ec7e5d7..eaf627c3731 100644 --- a/extensions/git/package.nls.json +++ b/extensions/git/package.nls.json @@ -163,10 +163,11 @@ "config.promptToSaveFilesBeforeCommit.always": "Check for any unsaved files.", "config.promptToSaveFilesBeforeCommit.staged": "Check only for unsaved staged files.", "config.promptToSaveFilesBeforeCommit.never": "Disable this check.", - "config.postCommitCommand": "Runs a git command after a successful commit.", + "config.postCommitCommand": "Run a git command after a successful commit.", "config.postCommitCommand.none": "Don't run any command after a commit.", - "config.postCommitCommand.push": "Run 'Git Push' after a successful commit.", - "config.postCommitCommand.sync": "Run 'Git Sync' after a successful commit.", + "config.postCommitCommand.push": "Run 'git push' after a successful commit.", + "config.postCommitCommand.sync": "Run 'git pull' and 'git push' after a successful commit.", + "config.rememberPostCommitCommand": "Remember the last git command that ran after a commit.", "config.openAfterClone": "Controls whether to open a repository automatically after cloning.", "config.openAfterClone.always": "Always open in current window.", "config.openAfterClone.alwaysNewWindow": "Always open in a new window.", diff --git a/extensions/git/src/actionButton.ts b/extensions/git/src/actionButton.ts index 69b24957282..7852c064225 100644 --- a/extensions/git/src/actionButton.ts +++ b/extensions/git/src/actionButton.ts @@ -5,9 +5,8 @@ import * as nls from 'vscode-nls'; import { Command, Disposable, Event, EventEmitter, SourceControlActionButton, Uri, workspace } from 'vscode'; -import { ApiRepository } from './api/api1'; -import { Branch, Status } from './api/git'; -import { IPostCommitCommandsProviderRegistry } from './postCommitCommands'; +import { Branch, CommitCommand, Status } from './api/git'; +import { CommitCommandsCenter } from './postCommitCommands'; import { Repository, Operation } from './repository'; import { dispose } from './util'; @@ -39,7 +38,7 @@ export class ActionButtonCommand { constructor( readonly repository: Repository, - readonly postCommitCommandsProviderRegistry: IPostCommitCommandsProviderRegistry) { + readonly postCommitCommandCenter: CommitCommandsCenter) { this._state = { HEAD: undefined, isCommitInProgress: false, @@ -52,7 +51,7 @@ export class ActionButtonCommand { repository.onDidRunGitStatus(this.onDidRunGitStatus, this, this.disposables); repository.onDidChangeOperations(this.onDidChangeOperations, this, this.disposables); - this.disposables.push(postCommitCommandsProviderRegistry.onDidChangePostCommitCommandsProviders(() => this._onDidChange.fire())); + this.disposables.push(postCommitCommandCenter.onDidChange(() => this._onDidChange.fire())); const root = Uri.file(repository.root); this.disposables.push(workspace.onDidChangeConfiguration(e => { @@ -65,6 +64,7 @@ export class ActionButtonCommand { if (e.affectsConfiguration('git.branchProtection', root) || e.affectsConfiguration('git.branchProtectionPrompt', root) || e.affectsConfiguration('git.postCommitCommand', root) || + e.affectsConfiguration('git.rememberPostCommitCommand', root) || e.affectsConfiguration('git.showActionButton', root)) { this._onDidChange.fire(); } @@ -92,14 +92,17 @@ export class ActionButtonCommand { // The button is disabled if (!showActionButton.commit) { return undefined; } + const primaryCommand = this.getCommitActionButtonPrimaryCommand(); + return { - command: this.getCommitActionButtonPrimaryCommand(), + command: primaryCommand, secondaryCommands: this.getCommitActionButtonSecondaryCommands(), + description: primaryCommand.description ?? primaryCommand.title, enabled: (this.state.repositoryHasChangesToCommit || this.state.isRebaseInProgress) && !this.state.isCommitInProgress && !this.state.isMergeInProgress }; } - private getCommitActionButtonPrimaryCommand(): Command { + private getCommitActionButtonPrimaryCommand(): CommitCommand { // Rebase Continue if (this.state.isRebaseInProgress) { return { @@ -111,87 +114,22 @@ export class ActionButtonCommand { } // Commit - const config = workspace.getConfiguration('git', Uri.file(this.repository.root)); - const postCommitCommand = config.get('postCommitCommand'); - - // Branch protection - const isBranchProtected = this.repository.isBranchProtected(); - const branchProtectionPrompt = config.get<'alwaysCommit' | 'alwaysCommitToNewBranch' | 'alwaysPrompt'>('branchProtectionPrompt')!; - const alwaysPrompt = isBranchProtected && branchProtectionPrompt === 'alwaysPrompt'; - const alwaysCommitToNewBranch = isBranchProtected && branchProtectionPrompt === 'alwaysCommitToNewBranch'; - - // Icon - const icon = alwaysPrompt ? '$(lock)' : alwaysCommitToNewBranch ? '$(git-branch)' : undefined; - - let commandArg = ''; - let title = localize('scm button commit title', "{0} Commit", icon ?? '$(check)'); - let tooltip = this.state.isCommitInProgress ? localize('scm button committing tooltip', "Committing Changes...") : localize('scm button commit tooltip', "Commit Changes"); - - // Title, tooltip - switch (postCommitCommand) { - case 'push': { - commandArg = 'git.push'; - title = localize('scm button commit and push title', "{0} Commit & Push", icon ?? '$(arrow-up)'); - if (alwaysCommitToNewBranch) { - tooltip = this.state.isCommitInProgress ? - localize('scm button committing to new branch and pushing tooltip', "Committing to New Branch & Pushing Changes...") : - localize('scm button commit to new branch and push tooltip', "Commit to New Branch & Push Changes"); - } else { - tooltip = this.state.isCommitInProgress ? - localize('scm button committing and pushing tooltip', "Committing & Pushing Changes...") : - localize('scm button commit and push tooltip', "Commit & Push Changes"); - } - break; - } - case 'sync': { - commandArg = 'git.sync'; - title = localize('scm button commit and sync title', "{0} Commit & Sync", icon ?? '$(sync)'); - if (alwaysCommitToNewBranch) { - tooltip = this.state.isCommitInProgress ? - localize('scm button committing to new branch and synching tooltip', "Committing to New Branch & Synching Changes...") : - localize('scm button commit to new branch and sync tooltip', "Commit to New Branch & Sync Changes"); - } else { - tooltip = this.state.isCommitInProgress ? - localize('scm button committing and synching tooltip', "Committing & Synching Changes...") : - localize('scm button commit and sync tooltip', "Commit & Sync Changes"); - } - break; - } - default: { - if (alwaysCommitToNewBranch) { - tooltip = this.state.isCommitInProgress ? - localize('scm button committing to new branch tooltip', "Committing Changes to New Branch...") : - localize('scm button commit to new branch tooltip', "Commit Changes to New Branch"); - } - break; - } - } - - return { command: 'git.commit', title, tooltip, arguments: [this.repository.sourceControl, commandArg] }; + return this.postCommitCommandCenter.getPrimaryCommand(); } private getCommitActionButtonSecondaryCommands(): Command[][] { + // Rebase Continue + if (this.state.isRebaseInProgress) { + return []; + } + + // Commit const commandGroups: Command[][] = []; - - if (!this.state.isRebaseInProgress) { - for (const provider of this.postCommitCommandsProviderRegistry.getPostCommitCommandsProviders()) { - const commands = provider.getCommands(new ApiRepository(this.repository)); - commandGroups.push((commands ?? []).map(c => { - return { - command: 'git.commit', - title: c.title, - arguments: [this.repository.sourceControl, c.command] - }; - })); - } - - if (commandGroups.length > 0) { - commandGroups[0].splice(0, 0, { - command: 'git.commit', - title: localize('scm secondary button commit', "Commit"), - arguments: [this.repository.sourceControl, ''] - }); - } + for (const commands of this.postCommitCommandCenter.getSecondaryCommands()) { + commandGroups.push(commands.map(c => { + // Use the description as title if present + return { command: 'git.commit', title: c.description ?? c.title, tooltip: c.tooltip, arguments: c.arguments }; + })); } return commandGroups; diff --git a/extensions/git/src/api/git.d.ts b/extensions/git/src/api/git.d.ts index cb6265558df..c67f5ab7118 100644 --- a/extensions/git/src/api/git.d.ts +++ b/extensions/git/src/api/git.d.ts @@ -254,8 +254,10 @@ export interface CredentialsProvider { getCredentials(host: Uri): ProviderResult; } +export type CommitCommand = Command & { description?: string }; + export interface PostCommitCommandsProvider { - getCommands(repository: Repository): Command[]; + getCommands(repository: Repository): CommitCommand[]; } export interface PushErrorHandler { diff --git a/extensions/git/src/commands.ts b/extensions/git/src/commands.ts index 656a18e885c..00827778a0e 100644 --- a/extensions/git/src/commands.ts +++ b/extensions/git/src/commands.ts @@ -1635,20 +1635,6 @@ export class CommandCenter { await repository.commit(message, opts); - // Execute post-commit command - let postCommitCommand = opts.postCommitCommand; - - if (postCommitCommand === undefined) { - // Commit WAS NOT initiated using the action button (ex: keybinding, toolbar - // action, command palette) so we honour the `git.postCommitCommand` setting. - const postCommitCommandSetting = config.get('postCommitCommand'); - postCommitCommand = postCommitCommandSetting === 'push' || postCommitCommandSetting === 'sync' ? `git.${postCommitCommandSetting}` : ''; - } - - if (postCommitCommand.length) { - await commands.executeCommand(postCommitCommand, new ApiRepository(repository)); - } - return true; } diff --git a/extensions/git/src/postCommitCommands.ts b/extensions/git/src/postCommitCommands.ts index 85d1689011a..eb669659d6a 100644 --- a/extensions/git/src/postCommitCommands.ts +++ b/extensions/git/src/postCommitCommands.ts @@ -4,8 +4,11 @@ *--------------------------------------------------------------------------------------------*/ import * as nls from 'vscode-nls'; -import { Command, Disposable, Event } from 'vscode'; -import { PostCommitCommandsProvider } from './api/git'; +import { commands, Disposable, Event, EventEmitter, Memento, Uri, workspace } from 'vscode'; +import { CommitCommand, PostCommitCommandsProvider } from './api/git'; +import { Operation, Repository } from './repository'; +import { ApiRepository } from './api/api1'; +import { dispose } from './util'; export interface IPostCommitCommandsProviderRegistry { readonly onDidChangePostCommitCommandsProviders: Event; @@ -17,16 +20,178 @@ export interface IPostCommitCommandsProviderRegistry { const localize = nls.loadMessageBundle(); export class GitPostCommitCommandsProvider implements PostCommitCommandsProvider { - getCommands(): Command[] { + getCommands(apiRepository: ApiRepository): CommitCommand[] { + const config = workspace.getConfiguration('git', Uri.file(apiRepository.repository.root)); + + // Branch protection + const isBranchProtected = apiRepository.repository.isBranchProtected(); + const branchProtectionPrompt = config.get<'alwaysCommit' | 'alwaysCommitToNewBranch' | 'alwaysPrompt'>('branchProtectionPrompt')!; + const alwaysPrompt = isBranchProtected && branchProtectionPrompt === 'alwaysPrompt'; + const alwaysCommitToNewBranch = isBranchProtected && branchProtectionPrompt === 'alwaysCommitToNewBranch'; + + // Icon + const icon = alwaysPrompt ? '$(lock)' : alwaysCommitToNewBranch ? '$(git-branch)' : undefined; + + // Tooltip (default) + let pushCommandTooltip = !alwaysCommitToNewBranch ? + localize('scm button commit and push tooltip', "Commit & Push Changes") : + localize('scm button commit to new branch and push tooltip', "Commit to New Branch & Push Changes"); + + let syncCommandTooltip = !alwaysCommitToNewBranch ? + localize('scm button commit and sync tooltip', "Commit & Sync Changes") : + localize('scm button commit to new branch and sync tooltip', "Commit to New Branch & Synchronize Changes"); + + // Tooltip (in progress) + if (apiRepository.repository.operations.isRunning(Operation.Commit)) { + pushCommandTooltip = !alwaysCommitToNewBranch ? + localize('scm button committing and pushing tooltip', "Committing & Pushing Changes...") : + localize('scm button committing to new branch and pushing tooltip', "Committing to New Branch & Pushing Changes..."); + + syncCommandTooltip = !alwaysCommitToNewBranch ? + localize('scm button committing and syncing tooltip', "Committing & Synchronizing Changes...") : + localize('scm button committing to new branch and syncing tooltip', "Committing to New Branch & Synchronizing Changes..."); + } + return [ { command: 'git.push', - title: localize('scm secondary button commit and push', "Commit & Push") + title: localize('scm button commit and push title', "{0} Commit", icon ?? '$(arrow-up)'), + description: localize('scm button commit and push description', "{0} Commit & Push", icon ?? '$(arrow-up)'), + tooltip: pushCommandTooltip }, { command: 'git.sync', - title: localize('scm secondary button commit and sync', "Commit & Sync") + title: localize('scm button commit and sync title', "{0} Commit", icon ?? '$(sync)'), + description: localize('scm button commit and sync description', "{0} Commit & Sync", icon ?? '$(sync)'), + tooltip: syncCommandTooltip }, ]; } } + +export class CommitCommandsCenter { + + private _onDidChange = new EventEmitter(); + get onDidChange(): Event { return this._onDidChange.event; } + + private disposables: Disposable[] = []; + + constructor( + private readonly globalState: Memento, + private readonly repository: Repository, + private readonly postCommitCommandsProviderRegistry: IPostCommitCommandsProviderRegistry + ) { + const root = Uri.file(repository.root); + this.disposables.push(workspace.onDidChangeConfiguration(async e => { + if (e.affectsConfiguration('git.rememberPostCommitCommand', root)) { + const config = workspace.getConfiguration('git', root); + if (!config.get('rememberPostCommitCommand')) { + await this.globalState.update(repository.root, undefined); + } + } + })); + + this.disposables.push(postCommitCommandsProviderRegistry.onDidChangePostCommitCommandsProviders(() => this._onDidChange.fire())); + } + + getPrimaryCommand(): CommitCommand { + const allCommands = this.getSecondaryCommands().map(c => c).flat(); + const commandFromStorage = allCommands.find(c => c.arguments?.length === 2 && c.arguments[1] === this.getPostCommitCommandStringFromStorage()); + const commandFromSetting = allCommands.find(c => c.arguments?.length === 2 && c.arguments[1] === this.getPostCommitCommandStringFromSetting()); + + return commandFromStorage ?? commandFromSetting ?? this.getCommitCommand(); + } + + getSecondaryCommands(): CommitCommand[][] { + const commandGroups: CommitCommand[][] = []; + + for (const provider of this.postCommitCommandsProviderRegistry.getPostCommitCommandsProviders()) { + const commands = provider.getCommands(new ApiRepository(this.repository)); + commandGroups.push((commands ?? []).map(c => { + return { command: 'git.commit', title: c.title, description: c.description, tooltip: c.tooltip, arguments: [this.repository.sourceControl, c.command] }; + })); + } + + if (commandGroups.length > 0) { + commandGroups[0].splice(0, 0, this.getCommitCommand()); + } + + return commandGroups; + } + + async executePostCommitCommand(command: string | undefined): Promise { + if (command === undefined) { + // Commit WAS NOT initiated using the action button (ex: keybinding, toolbar action, + // command palette) so we have to honour the default post commit command (memento/setting). + const primaryCommand = this.getPrimaryCommand(); + command = primaryCommand.arguments?.length === 2 ? primaryCommand.arguments[1] : ''; + } + + if (command?.length) { + await commands.executeCommand(command, new ApiRepository(this.repository)); + } + + await this.savePostCommitCommand(command); + } + + private getCommitCommand(): CommitCommand { + const config = workspace.getConfiguration('git', Uri.file(this.repository.root)); + + // Branch protection + const isBranchProtected = this.repository.isBranchProtected(); + const branchProtectionPrompt = config.get<'alwaysCommit' | 'alwaysCommitToNewBranch' | 'alwaysPrompt'>('branchProtectionPrompt')!; + const alwaysPrompt = isBranchProtected && branchProtectionPrompt === 'alwaysPrompt'; + const alwaysCommitToNewBranch = isBranchProtected && branchProtectionPrompt === 'alwaysCommitToNewBranch'; + + // Icon + const icon = alwaysPrompt ? '$(lock)' : alwaysCommitToNewBranch ? '$(git-branch)' : undefined; + + // Tooltip (default) + let tooltip = !alwaysCommitToNewBranch ? + localize('scm button commit tooltip', "Commit Changes") : + localize('scm button commit to new branch tooltip', "Commit Changes to New Branch"); + + // Tooltip (in progress) + if (this.repository.operations.isRunning(Operation.Commit)) { + tooltip = !alwaysCommitToNewBranch ? + localize('scm button committing tooltip', "Committing Changes...") : + localize('scm button committing to new branch tooltip', "Committing Changes to New Branch..."); + } + + return { command: 'git.commit', title: localize('scm button commit title', "{0} Commit", icon ?? '$(check)'), tooltip, arguments: [this.repository.sourceControl, ''] }; + } + + private getPostCommitCommandStringFromSetting(): string | undefined { + const config = workspace.getConfiguration('git', Uri.file(this.repository.root)); + const postCommitCommandSetting = config.get('postCommitCommand'); + + return postCommitCommandSetting === 'push' || postCommitCommandSetting === 'sync' ? `git.${postCommitCommandSetting}` : undefined; + } + + private getPostCommitCommandStringFromStorage(): string | undefined { + if (!this.isRememberPostCommitCommandEnabled()) { + return undefined; + } + + return this.globalState.get(this.repository.root); + } + + private isRememberPostCommitCommandEnabled(): boolean { + const config = workspace.getConfiguration('git', Uri.file(this.repository.root)); + return config.get('rememberPostCommitCommand') === true; + } + + private async savePostCommitCommand(command: string | undefined): Promise { + if (!this.isRememberPostCommitCommandEnabled()) { + return; + } + + command = command !== '' ? command : undefined; + await this.globalState.update(this.repository.root, command); + this._onDidChange.fire(); + } + + dispose(): void { + this.disposables = dispose(this.disposables); + } +} diff --git a/extensions/git/src/repository.ts b/extensions/git/src/repository.ts index 106dabee726..4dc1563bfb9 100644 --- a/extensions/git/src/repository.ts +++ b/extensions/git/src/repository.ts @@ -22,7 +22,7 @@ import { IPushErrorHandlerRegistry } from './pushError'; import { ApiRepository } from './api/api1'; import { IRemoteSourcePublisherRegistry } from './remotePublisher'; import { ActionButtonCommand } from './actionButton'; -import { IPostCommitCommandsProviderRegistry } from './postCommitCommands'; +import { IPostCommitCommandsProviderRegistry, CommitCommandsCenter } from './postCommitCommands'; const timeout = (millis: number) => new Promise(c => setTimeout(c, millis)); @@ -871,6 +871,7 @@ export class Repository implements Disposable { private didWarnAboutLimit = false; private isBranchProtectedMatcher: picomatch.Matcher | undefined; + private commitCommandCenter: CommitCommandsCenter; private resourceCommandResolver = new ResourceCommandResolver(this); private disposables: Disposable[] = []; @@ -999,7 +1000,10 @@ export class Repository implements Disposable { statusBar.onDidChange(() => this._sourceControl.statusBarCommands = statusBar.commands, null, this.disposables); this._sourceControl.statusBarCommands = statusBar.commands; - const actionButton = new ActionButtonCommand(this, postCommitCommandsProviderRegistry); + 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; @@ -1251,6 +1255,9 @@ export class Repository implements Disposable { await this.repository.commit(message, opts); this.closeDiffEditors(indexResources, workingGroupResources); }); + + // Execute post-commit command + await this.commitCommandCenter.executePostCommitCommand(opts.postCommitCommand); } } diff --git a/src/vs/base/browser/ui/button/button.ts b/src/vs/base/browser/ui/button/button.ts index 855ba9e0c90..b7aea2c66f1 100644 --- a/src/vs/base/browser/ui/button/button.ts +++ b/src/vs/base/browser/ui/button/button.ts @@ -263,7 +263,7 @@ export class ButtonWithDropdown extends Disposable implements IButton { this.element.classList.add('monaco-button-dropdown'); container.appendChild(this.element); - this.button = this._register(new Button(this.element, options)); + this.button = this._register(new ButtonWithDescription(this.element, options)); this._register(this.button.onDidClick(e => this._onDidClick.fire(e))); this.action = this._register(new Action('primaryAction', this.button.label, undefined, true, async () => this._onDidClick.fire(undefined))); @@ -300,6 +300,10 @@ export class ButtonWithDropdown extends Disposable implements IButton { this.button.icon = icon; } + set description(value: string) { + (this.button as ButtonWithDescription).description = value; + } + set enabled(enabled: boolean) { this.button.enabled = enabled; this.dropdownButton.enabled = enabled; diff --git a/src/vs/workbench/contrib/scm/browser/media/scm.css b/src/vs/workbench/contrib/scm/browser/media/scm.css index b379cc9add1..2482da45fd5 100644 --- a/src/vs/workbench/contrib/scm/browser/media/scm.css +++ b/src/vs/workbench/contrib/scm/browser/media/scm.css @@ -208,25 +208,25 @@ align-items: center; } -.scm-view .button-container > .monaco-description-button { +.scm-view .button-container .monaco-description-button { flex-direction: row; flex-wrap: wrap; padding: 0 4px; overflow: hidden; } -.scm-view .button-container > .monaco-description-button > .monaco-button-label { +.scm-view .button-container .monaco-description-button > .monaco-button-label { flex-grow: 1; width: 0; overflow: hidden; } -.scm-view .button-container > .monaco-description-button > .monaco-button-description { +.scm-view .button-container .monaco-description-button > .monaco-button-description { flex-basis: 100%; } -.scm-view .button-container > .monaco-description-button > .monaco-button-label, -.scm-view .button-container > .monaco-description-button > .monaco-button-description { +.scm-view .button-container .monaco-description-button > .monaco-button-label, +.scm-view .button-container .monaco-description-button > .monaco-button-description { font-style: inherit; padding: 4px 0; } @@ -246,6 +246,7 @@ .scm-view .button-container > .monaco-button-dropdown { flex-grow: 1; + overflow: hidden; } .scm-view .button-container > .monaco-button-dropdown > .monaco-dropdown-button { diff --git a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts index f80ccb67fb0..f580b096c59 100644 --- a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts +++ b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts @@ -2561,7 +2561,7 @@ registerThemingParticipant((theme, collector) => { } const buttonBorderColor = theme.getColor(buttonBorder); - collector.addRule(`.scm-view .button-container > .monaco-description-button { height: ${buttonBorderColor ? '32px' : '30px'}; }`); + collector.addRule(`.scm-view .button-container .monaco-description-button { height: ${buttonBorderColor ? '32px' : '30px'}; }`); const focusBorderColor = theme.getColor(focusBorder); if (focusBorderColor) { @@ -2670,6 +2670,9 @@ export class SCMActionButton implements IDisposable { title: button.command.tooltip, supportIcons: true }); + if (button.description) { + (this.button as ButtonWithDropdown).description = button.description; + } } else if (button.description) { // ButtonWithDescription this.button = new ButtonWithDescription(this.container, { supportIcons: true, title: button.command.tooltip });