Path: blob/main/extensions/git/src/postCommitCommands.ts
3316 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { Command, commands, Disposable, Event, EventEmitter, Memento, Uri, workspace, l10n } from 'vscode';6import { PostCommitCommandsProvider } from './api/git';7import { IRepositoryResolver, Repository } from './repository';8import { ApiRepository } from './api/api1';9import { dispose } from './util';10import { OperationKind } from './operation';1112export interface IPostCommitCommandsProviderRegistry {13readonly onDidChangePostCommitCommandsProviders: Event<void>;1415getPostCommitCommandsProviders(): PostCommitCommandsProvider[];16registerPostCommitCommandsProvider(provider: PostCommitCommandsProvider): Disposable;17}1819export class GitPostCommitCommandsProvider implements PostCommitCommandsProvider {20constructor(private readonly _repositoryResolver: IRepositoryResolver) { }2122getCommands(apiRepository: ApiRepository): Command[] {23const repository = this._repositoryResolver.getRepository(apiRepository.rootUri);24if (!repository) {25return [];26}2728const config = workspace.getConfiguration('git', Uri.file(repository.root));2930// Branch protection31const isBranchProtected = repository.isBranchProtected();32const branchProtectionPrompt = config.get<'alwaysCommit' | 'alwaysCommitToNewBranch' | 'alwaysPrompt'>('branchProtectionPrompt')!;33const alwaysPrompt = isBranchProtected && branchProtectionPrompt === 'alwaysPrompt';34const alwaysCommitToNewBranch = isBranchProtected && branchProtectionPrompt === 'alwaysCommitToNewBranch';3536// Icon37const isCommitInProgress = repository.operations.isRunning(OperationKind.Commit) || repository.operations.isRunning(OperationKind.PostCommitCommand);38const icon = isCommitInProgress ? '$(sync~spin)' : alwaysPrompt ? '$(lock)' : alwaysCommitToNewBranch ? '$(git-branch)' : undefined;3940// Tooltip (default)41let pushCommandTooltip = !alwaysCommitToNewBranch ?42l10n.t('Commit & Push Changes') :43l10n.t('Commit to New Branch & Push Changes');4445let syncCommandTooltip = !alwaysCommitToNewBranch ?46l10n.t('Commit & Sync Changes') :47l10n.t('Commit to New Branch & Synchronize Changes');4849// Tooltip (in progress)50if (isCommitInProgress) {51pushCommandTooltip = !alwaysCommitToNewBranch ?52l10n.t('Committing & Pushing Changes...') :53l10n.t('Committing to New Branch & Pushing Changes...');5455syncCommandTooltip = !alwaysCommitToNewBranch ?56l10n.t('Committing & Synchronizing Changes...') :57l10n.t('Committing to New Branch & Synchronizing Changes...');58}5960return [61{62command: 'git.push',63title: l10n.t('{0} Commit & Push', icon ?? '$(arrow-up)'),64tooltip: pushCommandTooltip65},66{67command: 'git.sync',68title: l10n.t('{0} Commit & Sync', icon ?? '$(sync)'),69tooltip: syncCommandTooltip70},71];72}73}7475export class CommitCommandsCenter {7677private _onDidChange = new EventEmitter<void>();78get onDidChange(): Event<void> { return this._onDidChange.event; }7980private disposables: Disposable[] = [];8182set postCommitCommand(command: string | null | undefined) {83if (command === undefined) {84// Commit WAS NOT initiated using the action button85// so there is no need to store the post-commit command86return;87}8889this.globalState.update(this.getGlobalStateKey(), command)90.then(() => this._onDidChange.fire());91}9293constructor(94private readonly globalState: Memento,95private readonly repository: Repository,96private readonly postCommitCommandsProviderRegistry: IPostCommitCommandsProviderRegistry97) {98const root = Uri.file(repository.root);99100// Migrate post commit command storage101this.migratePostCommitCommandStorage()102.then(() => {103const onRememberPostCommitCommandChange = async () => {104const config = workspace.getConfiguration('git', root);105if (!config.get<boolean>('rememberPostCommitCommand')) {106await this.globalState.update(this.getGlobalStateKey(), undefined);107}108};109this.disposables.push(workspace.onDidChangeConfiguration(e => {110if (e.affectsConfiguration('git.rememberPostCommitCommand', root)) {111onRememberPostCommitCommandChange();112}113}));114onRememberPostCommitCommandChange();115116this.disposables.push(postCommitCommandsProviderRegistry.onDidChangePostCommitCommandsProviders(() => this._onDidChange.fire()));117});118}119120getPrimaryCommand(): Command {121const allCommands = this.getSecondaryCommands().map(c => c).flat();122const commandFromStorage = allCommands.find(c => c.arguments?.length === 2 && c.arguments[1] === this.getPostCommitCommandStringFromStorage());123const commandFromSetting = allCommands.find(c => c.arguments?.length === 2 && c.arguments[1] === this.getPostCommitCommandStringFromSetting());124125return commandFromStorage ?? commandFromSetting ?? this.getCommitCommands()[0];126}127128getSecondaryCommands(): Command[][] {129const commandGroups: Command[][] = [];130131for (const provider of this.postCommitCommandsProviderRegistry.getPostCommitCommandsProviders()) {132const commands = provider.getCommands(new ApiRepository(this.repository));133commandGroups.push((commands ?? []).map(c => {134return { command: 'git.commit', title: c.title, tooltip: c.tooltip, arguments: [this.repository.sourceControl, c.command] };135}));136}137138if (commandGroups.length > 0) {139commandGroups.splice(0, 0, this.getCommitCommands());140}141142return commandGroups;143}144145async executePostCommitCommand(command: string | null | undefined): Promise<void> {146try {147if (command === null) {148// No post-commit command149return;150}151152if (command === undefined) {153// Commit WAS NOT initiated using the action button (ex: keybinding, toolbar action,154// command palette) so we have to honour the default post commit command (memento/setting).155const primaryCommand = this.getPrimaryCommand();156command = primaryCommand.arguments?.length === 2 ? primaryCommand.arguments[1] : null;157}158159if (command !== null) {160await commands.executeCommand(command!.toString(), new ApiRepository(this.repository));161}162} catch (err) {163throw err;164}165finally {166if (!this.isRememberPostCommitCommandEnabled()) {167await this.globalState.update(this.getGlobalStateKey(), undefined);168this._onDidChange.fire();169}170}171}172173private getGlobalStateKey(): string {174return `postCommitCommand:${this.repository.root}`;175}176177private getCommitCommands(): Command[] {178const config = workspace.getConfiguration('git', Uri.file(this.repository.root));179180// Branch protection181const isBranchProtected = this.repository.isBranchProtected();182const branchProtectionPrompt = config.get<'alwaysCommit' | 'alwaysCommitToNewBranch' | 'alwaysPrompt'>('branchProtectionPrompt')!;183const alwaysPrompt = isBranchProtected && branchProtectionPrompt === 'alwaysPrompt';184const alwaysCommitToNewBranch = isBranchProtected && branchProtectionPrompt === 'alwaysCommitToNewBranch';185186// Icon187const icon = alwaysPrompt ? '$(lock)' : alwaysCommitToNewBranch ? '$(git-branch)' : undefined;188189// Tooltip (default)190const branch = this.repository.HEAD?.name;191let tooltip = alwaysCommitToNewBranch ?192l10n.t('Commit Changes to New Branch') :193branch ?194l10n.t('Commit Changes on "{0}"', branch) :195l10n.t('Commit Changes');196197// Tooltip (in progress)198if (this.repository.operations.isRunning(OperationKind.Commit)) {199tooltip = !alwaysCommitToNewBranch ?200l10n.t('Committing Changes...') :201l10n.t('Committing Changes to New Branch...');202}203204return [205{ command: 'git.commit', title: l10n.t('{0} Commit', icon ?? '$(check)'), tooltip, arguments: [this.repository.sourceControl, null] },206{ command: 'git.commitAmend', title: l10n.t('{0} Commit (Amend)', icon ?? '$(check)'), tooltip, arguments: [this.repository.sourceControl, null] },207];208}209210private getPostCommitCommandStringFromSetting(): string | undefined {211const config = workspace.getConfiguration('git', Uri.file(this.repository.root));212const postCommitCommandSetting = config.get<string>('postCommitCommand');213214return postCommitCommandSetting === 'push' || postCommitCommandSetting === 'sync' ? `git.${postCommitCommandSetting}` : undefined;215}216217private getPostCommitCommandStringFromStorage(): string | null | undefined {218return this.globalState.get<string | null>(this.getGlobalStateKey());219}220221private async migratePostCommitCommandStorage(): Promise<void> {222const postCommitCommandString = this.globalState.get<string | null>(this.repository.root);223224if (postCommitCommandString !== undefined) {225await this.globalState.update(this.getGlobalStateKey(), postCommitCommandString);226await this.globalState.update(this.repository.root, undefined);227}228}229230private isRememberPostCommitCommandEnabled(): boolean {231const config = workspace.getConfiguration('git', Uri.file(this.repository.root));232return config.get<boolean>('rememberPostCommitCommand') === true;233}234235dispose(): void {236this.disposables = dispose(this.disposables);237}238}239240241