Git - Add setting to remember post commit command (#158449)

This commit is contained in:
Ladislau Szomoru 2022-08-22 12:34:47 +02:00 committed by GitHub
parent 45d2dd8c54
commit 3cfc74c52e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 229 additions and 116 deletions

View file

@ -2192,6 +2192,12 @@
"scope": "resource",
"default": "none"
},
"git.rememberPostCommitCommand": {
"type": "boolean",
"description": "%config.rememberPostCommitCommand%",
"scope": "resource",
"default": false
},
"git.openAfterClone": {
"type": "string",
"enum": [

View file

@ -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.",

View file

@ -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<string>('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;

View file

@ -254,8 +254,10 @@ export interface CredentialsProvider {
getCredentials(host: Uri): ProviderResult<Credentials>;
}
export type CommitCommand = Command & { description?: string };
export interface PostCommitCommandsProvider {
getCommands(repository: Repository): Command[];
getCommands(repository: Repository): CommitCommand[];
}
export interface PushErrorHandler {

View file

@ -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<string>('postCommitCommand');
postCommitCommand = postCommitCommandSetting === 'push' || postCommitCommandSetting === 'sync' ? `git.${postCommitCommandSetting}` : '';
}
if (postCommitCommand.length) {
await commands.executeCommand(postCommitCommand, new ApiRepository(repository));
}
return true;
}

View file

@ -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<void>;
@ -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<void>();
get onDidChange(): Event<void> { 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<boolean>('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<void> {
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<string>('postCommitCommand');
return postCommitCommandSetting === 'push' || postCommitCommandSetting === 'sync' ? `git.${postCommitCommandSetting}` : undefined;
}
private getPostCommitCommandStringFromStorage(): string | undefined {
if (!this.isRememberPostCommitCommandEnabled()) {
return undefined;
}
return this.globalState.get<string>(this.repository.root);
}
private isRememberPostCommitCommandEnabled(): boolean {
const config = workspace.getConfiguration('git', Uri.file(this.repository.root));
return config.get<boolean>('rememberPostCommitCommand') === true;
}
private async savePostCommitCommand(command: string | undefined): Promise<void> {
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);
}
}

View file

@ -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);
}
}

View file

@ -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;

View file

@ -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 {

View file

@ -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 });