mirror of
https://github.com/Microsoft/vscode
synced 2024-10-01 08:50:48 +00:00
Git - use editor as commit message input (#151491)
This commit is contained in:
parent
793b0fd550
commit
6f5fc17622
|
@ -13,6 +13,7 @@ module.exports = withDefaults({
|
|||
context: __dirname,
|
||||
entry: {
|
||||
main: './src/main.ts',
|
||||
['askpass-main']: './src/askpass-main.ts'
|
||||
['askpass-main']: './src/askpass-main.ts',
|
||||
['git-editor-main']: './src/git-editor-main.ts'
|
||||
}
|
||||
});
|
||||
|
|
|
@ -14,6 +14,7 @@
|
|||
"contribMergeEditorToolbar",
|
||||
"contribViewsWelcome",
|
||||
"scmActionButton",
|
||||
"scmInput",
|
||||
"scmSelectedProvider",
|
||||
"scmValidation",
|
||||
"timeline"
|
||||
|
@ -213,83 +214,99 @@
|
|||
"command": "git.commit",
|
||||
"title": "%command.commit%",
|
||||
"category": "Git",
|
||||
"icon": "$(check)"
|
||||
"icon": "$(check)",
|
||||
"enablement": "!commitInProgress"
|
||||
},
|
||||
{
|
||||
"command": "git.commitStaged",
|
||||
"title": "%command.commitStaged%",
|
||||
"category": "Git"
|
||||
"category": "Git",
|
||||
"enablement": "!commitInProgress"
|
||||
},
|
||||
{
|
||||
"command": "git.commitEmpty",
|
||||
"title": "%command.commitEmpty%",
|
||||
"category": "Git"
|
||||
"category": "Git",
|
||||
"enablement": "!commitInProgress"
|
||||
},
|
||||
{
|
||||
"command": "git.commitStagedSigned",
|
||||
"title": "%command.commitStagedSigned%",
|
||||
"category": "Git"
|
||||
"category": "Git",
|
||||
"enablement": "!commitInProgress"
|
||||
},
|
||||
{
|
||||
"command": "git.commitStagedAmend",
|
||||
"title": "%command.commitStagedAmend%",
|
||||
"category": "Git"
|
||||
"category": "Git",
|
||||
"enablement": "!commitInProgress"
|
||||
},
|
||||
{
|
||||
"command": "git.commitAll",
|
||||
"title": "%command.commitAll%",
|
||||
"category": "Git"
|
||||
"category": "Git",
|
||||
"enablement": "!commitInProgress"
|
||||
},
|
||||
{
|
||||
"command": "git.commitAllSigned",
|
||||
"title": "%command.commitAllSigned%",
|
||||
"category": "Git"
|
||||
"category": "Git",
|
||||
"enablement": "!commitInProgress"
|
||||
},
|
||||
{
|
||||
"command": "git.commitAllAmend",
|
||||
"title": "%command.commitAllAmend%",
|
||||
"category": "Git"
|
||||
"category": "Git",
|
||||
"enablement": "!commitInProgress"
|
||||
},
|
||||
{
|
||||
"command": "git.commitNoVerify",
|
||||
"title": "%command.commitNoVerify%",
|
||||
"category": "Git",
|
||||
"icon": "$(check)"
|
||||
"icon": "$(check)",
|
||||
"enablement": "!commitInProgress"
|
||||
},
|
||||
{
|
||||
"command": "git.commitStagedNoVerify",
|
||||
"title": "%command.commitStagedNoVerify%",
|
||||
"category": "Git"
|
||||
"category": "Git",
|
||||
"enablement": "!commitInProgress"
|
||||
},
|
||||
{
|
||||
"command": "git.commitEmptyNoVerify",
|
||||
"title": "%command.commitEmptyNoVerify%",
|
||||
"category": "Git"
|
||||
"category": "Git",
|
||||
"enablement": "!commitInProgress"
|
||||
},
|
||||
{
|
||||
"command": "git.commitStagedSignedNoVerify",
|
||||
"title": "%command.commitStagedSignedNoVerify%",
|
||||
"category": "Git"
|
||||
"category": "Git",
|
||||
"enablement": "!commitInProgress"
|
||||
},
|
||||
{
|
||||
"command": "git.commitStagedAmendNoVerify",
|
||||
"title": "%command.commitStagedAmendNoVerify%",
|
||||
"category": "Git"
|
||||
"category": "Git",
|
||||
"enablement": "!commitInProgress"
|
||||
},
|
||||
{
|
||||
"command": "git.commitAllNoVerify",
|
||||
"title": "%command.commitAllNoVerify%",
|
||||
"category": "Git"
|
||||
"category": "Git",
|
||||
"enablement": "!commitInProgress"
|
||||
},
|
||||
{
|
||||
"command": "git.commitAllSignedNoVerify",
|
||||
"title": "%command.commitAllSignedNoVerify%",
|
||||
"category": "Git"
|
||||
"category": "Git",
|
||||
"enablement": "!commitInProgress"
|
||||
},
|
||||
{
|
||||
"command": "git.commitAllAmendNoVerify",
|
||||
"title": "%command.commitAllAmendNoVerify%",
|
||||
"category": "Git"
|
||||
"category": "Git",
|
||||
"enablement": "!commitInProgress"
|
||||
},
|
||||
{
|
||||
"command": "git.restoreCommitTemplate",
|
||||
|
@ -2013,6 +2030,18 @@
|
|||
"scope": "machine",
|
||||
"description": "%config.defaultCloneDirectory%"
|
||||
},
|
||||
"git.useEditorAsCommitInput": {
|
||||
"type": "boolean",
|
||||
"scope": "resource",
|
||||
"description": "%config.useEditorAsCommitInput%",
|
||||
"default": false
|
||||
},
|
||||
"git.verboseCommit": {
|
||||
"type": "boolean",
|
||||
"scope": "resource",
|
||||
"markdownDescription": "%config.verboseCommit%",
|
||||
"default": false
|
||||
},
|
||||
"git.enableSmartCommit": {
|
||||
"type": "boolean",
|
||||
"scope": "resource",
|
||||
|
|
|
@ -140,6 +140,8 @@
|
|||
"config.ignoreLimitWarning": "Ignores the warning when there are too many changes in a repository.",
|
||||
"config.ignoreRebaseWarning": "Ignores the warning when it looks like the branch might have been rebased when pulling.",
|
||||
"config.defaultCloneDirectory": "The default location to clone a git repository.",
|
||||
"config.useEditorAsCommitInput": "Use an editor to author the commit message.",
|
||||
"config.verboseCommit": "Enable verbose output when `#git.useEditorAsCommitInput#` is enabled.",
|
||||
"config.enableSmartCommit": "Commit all changes when there are no staged changes.",
|
||||
"config.smartCommitChanges": "Control which changes are automatically staged by Smart Commit.",
|
||||
"config.smartCommitChanges.all": "Automatically stage all changes.",
|
||||
|
|
3
extensions/git/src/api/git.d.ts
vendored
3
extensions/git/src/api/git.d.ts
vendored
|
@ -137,6 +137,8 @@ export interface CommitOptions {
|
|||
empty?: boolean;
|
||||
noVerify?: boolean;
|
||||
requireUserConfig?: boolean;
|
||||
useEditor?: boolean;
|
||||
verbose?: boolean;
|
||||
}
|
||||
|
||||
export interface FetchOptions {
|
||||
|
@ -336,4 +338,5 @@ export const enum GitErrorCodes {
|
|||
PatchDoesNotApply = 'PatchDoesNotApply',
|
||||
NoPathFound = 'NoPathFound',
|
||||
UnknownPath = 'UnknownPath',
|
||||
EmptyCommitMessage = 'EmptyCommitMessage'
|
||||
}
|
||||
|
|
|
@ -6,9 +6,8 @@
|
|||
import { window, InputBoxOptions, Uri, Disposable, workspace } from 'vscode';
|
||||
import { IDisposable, EmptyDisposable, toDisposable } from './util';
|
||||
import * as path from 'path';
|
||||
import { IIPCHandler, IIPCServer, createIPCServer } from './ipc/ipcServer';
|
||||
import { IIPCHandler, IIPCServer } from './ipc/ipcServer';
|
||||
import { CredentialsProvider, Credentials } from './api/git';
|
||||
import { OutputChannelLogger } from './log';
|
||||
|
||||
export class Askpass implements IIPCHandler {
|
||||
|
||||
|
@ -16,16 +15,7 @@ export class Askpass implements IIPCHandler {
|
|||
private cache = new Map<string, Credentials>();
|
||||
private credentialsProviders = new Set<CredentialsProvider>();
|
||||
|
||||
static async create(outputChannelLogger: OutputChannelLogger, context?: string): Promise<Askpass> {
|
||||
try {
|
||||
return new Askpass(await createIPCServer(context));
|
||||
} catch (err) {
|
||||
outputChannelLogger.logError(`Failed to create git askpass IPC: ${err}`);
|
||||
return new Askpass();
|
||||
}
|
||||
}
|
||||
|
||||
private constructor(private ipc?: IIPCServer) {
|
||||
constructor(private ipc?: IIPCServer) {
|
||||
if (ipc) {
|
||||
this.disposable = ipc.registerHandler('askpass', this);
|
||||
}
|
||||
|
|
|
@ -1516,6 +1516,14 @@ export class CommandCenter {
|
|||
opts.signoff = true;
|
||||
}
|
||||
|
||||
if (config.get<boolean>('useEditorAsCommitInput')) {
|
||||
opts.useEditor = true;
|
||||
|
||||
if (config.get<boolean>('verboseCommit')) {
|
||||
opts.verbose = true;
|
||||
}
|
||||
}
|
||||
|
||||
const smartCommitChanges = config.get<'all' | 'tracked'>('smartCommitChanges');
|
||||
|
||||
if (
|
||||
|
@ -1563,7 +1571,7 @@ export class CommandCenter {
|
|||
|
||||
let message = await getCommitMessage();
|
||||
|
||||
if (!message && !opts.amend) {
|
||||
if (!message && !opts.amend && !opts.useEditor) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -1623,10 +1631,13 @@ export class CommandCenter {
|
|||
|
||||
private async commitWithAnyInput(repository: Repository, opts?: CommitOptions): Promise<void> {
|
||||
const message = repository.inputBox.value;
|
||||
const root = Uri.file(repository.root);
|
||||
const config = workspace.getConfiguration('git', root);
|
||||
|
||||
const getCommitMessage = async () => {
|
||||
let _message: string | undefined = message;
|
||||
|
||||
if (!_message) {
|
||||
if (!_message && !config.get<boolean>('useEditorAsCommitInput')) {
|
||||
let value: string | undefined = undefined;
|
||||
|
||||
if (opts && opts.amend && repository.HEAD && repository.HEAD.commit) {
|
||||
|
@ -3010,7 +3021,7 @@ export class CommandCenter {
|
|||
};
|
||||
|
||||
let message: string;
|
||||
let type: 'error' | 'warning' = 'error';
|
||||
let type: 'error' | 'warning' | 'information' = 'error';
|
||||
|
||||
const choices = new Map<string, () => void>();
|
||||
const openOutputChannelChoice = localize('open git log', "Open Git Log");
|
||||
|
@ -3073,6 +3084,12 @@ export class CommandCenter {
|
|||
message = localize('missing user info', "Make sure you configure your 'user.name' and 'user.email' in git.");
|
||||
choices.set(localize('learn more', "Learn More"), () => commands.executeCommand('vscode.open', Uri.parse('https://aka.ms/vscode-setup-git')));
|
||||
break;
|
||||
case GitErrorCodes.EmptyCommitMessage:
|
||||
message = localize('empty commit', "Commit operation was cancelled due to empty commit message.");
|
||||
choices.clear();
|
||||
type = 'information';
|
||||
options.modal = false;
|
||||
break;
|
||||
default: {
|
||||
const hint = (err.stderr || err.message || String(err))
|
||||
.replace(/^error: /mi, '')
|
||||
|
@ -3094,10 +3111,20 @@ export class CommandCenter {
|
|||
return;
|
||||
}
|
||||
|
||||
let result: string | undefined;
|
||||
const allChoices = Array.from(choices.keys());
|
||||
const result = type === 'error'
|
||||
? await window.showErrorMessage(message, options, ...allChoices)
|
||||
: await window.showWarningMessage(message, options, ...allChoices);
|
||||
|
||||
switch (type) {
|
||||
case 'error':
|
||||
result = await window.showErrorMessage(message, options, ...allChoices);
|
||||
break;
|
||||
case 'warning':
|
||||
result = await window.showWarningMessage(message, options, ...allChoices);
|
||||
break;
|
||||
case 'information':
|
||||
result = await window.showInformationMessage(message, options, ...allChoices);
|
||||
break;
|
||||
}
|
||||
|
||||
if (result) {
|
||||
const resultFn = choices.get(result);
|
||||
|
|
1
extensions/git/src/git-editor-empty.sh
Executable file
1
extensions/git/src/git-editor-empty.sh
Executable file
|
@ -0,0 +1 @@
|
|||
#!/bin/sh
|
21
extensions/git/src/git-editor-main.ts
Normal file
21
extensions/git/src/git-editor-main.ts
Normal file
|
@ -0,0 +1,21 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
import { IPCClient } from './ipc/ipcClient';
|
||||
|
||||
function fatal(err: any): void {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
function main(argv: string[]): void {
|
||||
const ipcClient = new IPCClient('git-editor');
|
||||
const commitMessagePath = argv[argv.length - 1];
|
||||
|
||||
ipcClient.call({ commitMessagePath }).then(() => {
|
||||
setTimeout(() => process.exit(0), 0);
|
||||
}).catch(err => fatal(err));
|
||||
}
|
||||
|
||||
main(process.argv);
|
4
extensions/git/src/git-editor.sh
Executable file
4
extensions/git/src/git-editor.sh
Executable file
|
@ -0,0 +1,4 @@
|
|||
#!/bin/sh
|
||||
|
||||
ELECTRON_RUN_AS_NODE="1" \
|
||||
"$VSCODE_GIT_EDITOR_NODE" "$VSCODE_GIT_EDITOR_MAIN" $VSCODE_GIT_EDITOR_EXTRA_ARGS $@
|
|
@ -1400,20 +1400,37 @@ export class Repository {
|
|||
}
|
||||
|
||||
async commit(message: string | undefined, opts: CommitOptions = Object.create(null)): Promise<void> {
|
||||
const args = ['commit', '--quiet', '--allow-empty-message'];
|
||||
const args = ['commit', '--quiet'];
|
||||
const options: SpawnOptions = {};
|
||||
|
||||
if (message) {
|
||||
options.input = message;
|
||||
args.push('--file', '-');
|
||||
}
|
||||
|
||||
if (opts.verbose) {
|
||||
args.push('--verbose');
|
||||
}
|
||||
|
||||
if (opts.all) {
|
||||
args.push('--all');
|
||||
}
|
||||
|
||||
if (opts.amend && message) {
|
||||
if (opts.amend) {
|
||||
args.push('--amend');
|
||||
}
|
||||
|
||||
if (opts.amend && !message) {
|
||||
args.push('--amend', '--no-edit');
|
||||
} else {
|
||||
args.push('--file', '-');
|
||||
if (!opts.useEditor) {
|
||||
if (!message) {
|
||||
if (opts.amend) {
|
||||
args.push('--no-edit');
|
||||
} else {
|
||||
options.input = '';
|
||||
args.push('--file', '-');
|
||||
}
|
||||
}
|
||||
|
||||
args.push('--allow-empty-message');
|
||||
}
|
||||
|
||||
if (opts.signoff) {
|
||||
|
@ -1438,7 +1455,7 @@ export class Repository {
|
|||
}
|
||||
|
||||
try {
|
||||
await this.exec(args, !opts.amend || message ? { input: message || '' } : {});
|
||||
await this.exec(args, options);
|
||||
} catch (commitErr) {
|
||||
await this.handleCommitError(commitErr);
|
||||
}
|
||||
|
@ -1462,6 +1479,9 @@ export class Repository {
|
|||
if (/not possible because you have unmerged files/.test(commitErr.stderr || '')) {
|
||||
commitErr.gitErrorCode = GitErrorCodes.UnmergedChanges;
|
||||
throw commitErr;
|
||||
} else if (/Aborting commit due to empty commit message/.test(commitErr.stderr || '')) {
|
||||
commitErr.gitErrorCode = GitErrorCodes.EmptyCommitMessage;
|
||||
throw commitErr;
|
||||
}
|
||||
|
||||
try {
|
||||
|
|
65
extensions/git/src/gitEditor.ts
Normal file
65
extensions/git/src/gitEditor.ts
Normal file
|
@ -0,0 +1,65 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
import * as path from 'path';
|
||||
import { TabInputText, Uri, window, workspace } from 'vscode';
|
||||
import { IIPCHandler, IIPCServer } from './ipc/ipcServer';
|
||||
import { EmptyDisposable, IDisposable } from './util';
|
||||
|
||||
interface GitEditorRequest {
|
||||
commitMessagePath?: string;
|
||||
}
|
||||
|
||||
export class GitEditor implements IIPCHandler {
|
||||
|
||||
private disposable: IDisposable = EmptyDisposable;
|
||||
|
||||
constructor(private ipc?: IIPCServer) {
|
||||
if (ipc) {
|
||||
this.disposable = ipc.registerHandler('git-editor', this);
|
||||
}
|
||||
}
|
||||
|
||||
async handle({ commitMessagePath }: GitEditorRequest): Promise<any> {
|
||||
if (commitMessagePath) {
|
||||
const uri = Uri.file(commitMessagePath);
|
||||
const doc = await workspace.openTextDocument(uri);
|
||||
await window.showTextDocument(doc, { preview: false });
|
||||
|
||||
return new Promise((c) => {
|
||||
const onDidClose = window.tabGroups.onDidChangeTabs(async (tabs) => {
|
||||
if (tabs.closed.some(t => t.input instanceof TabInputText && t.input.uri.toString() === uri.toString())) {
|
||||
onDidClose.dispose();
|
||||
return c(true);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
getEnv(): { [key: string]: string } {
|
||||
if (!this.ipc) {
|
||||
return {
|
||||
GIT_EDITOR: `"${path.join(__dirname, 'git-editor-empty.sh')}"`
|
||||
};
|
||||
}
|
||||
|
||||
let env: { [key: string]: string } = {
|
||||
VSCODE_GIT_EDITOR_NODE: process.execPath,
|
||||
VSCODE_GIT_EDITOR_EXTRA_ARGS: (process.versions['electron'] && process.versions['microsoft-build']) ? '--ms-enable-electron-run-as-node' : '',
|
||||
VSCODE_GIT_EDITOR_MAIN: path.join(__dirname, 'git-editor-main.js')
|
||||
};
|
||||
|
||||
const config = workspace.getConfiguration('git');
|
||||
if (config.get<boolean>('useEditorAsCommitInput')) {
|
||||
env.GIT_EDITOR = `"${path.join(__dirname, 'git-editor.sh')}"`;
|
||||
}
|
||||
|
||||
return env;
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.disposable.dispose();
|
||||
}
|
||||
}
|
|
@ -25,6 +25,8 @@ import { GitTimelineProvider } from './timelineProvider';
|
|||
import { registerAPICommands } from './api/api1';
|
||||
import { TerminalEnvironmentManager } from './terminal';
|
||||
import { OutputChannelLogger } from './log';
|
||||
import { createIPCServer, IIPCServer } from './ipc/ipcServer';
|
||||
import { GitEditor } from './gitEditor';
|
||||
|
||||
const deactivateTasks: { (): Promise<any> }[] = [];
|
||||
|
||||
|
@ -60,10 +62,21 @@ async function createModel(context: ExtensionContext, outputChannelLogger: Outpu
|
|||
return !skip;
|
||||
});
|
||||
|
||||
const askpass = await Askpass.create(outputChannelLogger, context.storagePath);
|
||||
let ipc: IIPCServer | undefined = undefined;
|
||||
|
||||
try {
|
||||
ipc = await createIPCServer(context.storagePath);
|
||||
} catch (err) {
|
||||
outputChannelLogger.logError(`Failed to create git IPC: ${err}`);
|
||||
}
|
||||
|
||||
const askpass = new Askpass(ipc);
|
||||
disposables.push(askpass);
|
||||
|
||||
const environment = askpass.getEnv();
|
||||
const gitEditor = new GitEditor(ipc);
|
||||
disposables.push(gitEditor);
|
||||
|
||||
const environment = { ...askpass.getEnv(), ...gitEditor.getEnv() };
|
||||
const terminalEnvironmentManager = new TerminalEnvironmentManager(context, environment);
|
||||
disposables.push(terminalEnvironmentManager);
|
||||
|
||||
|
|
|
@ -454,6 +454,13 @@ class ProgressManager {
|
|||
const onDidChange = filterEvent(workspace.onDidChangeConfiguration, e => e.affectsConfiguration('git', Uri.file(this.repository.root)));
|
||||
onDidChange(_ => this.updateEnablement());
|
||||
this.updateEnablement();
|
||||
|
||||
this.repository.onDidChangeOperations(() => {
|
||||
const commitInProgress = this.repository.operations.isRunning(Operation.Commit);
|
||||
|
||||
this.repository.sourceControl.inputBox.enabled = !commitInProgress;
|
||||
commands.executeCommand('setContext', 'commitInProgress', commitInProgress);
|
||||
});
|
||||
}
|
||||
|
||||
private updateEnablement(): void {
|
||||
|
|
|
@ -12,6 +12,7 @@
|
|||
"../../src/vscode-dts/vscode.d.ts",
|
||||
"../../src/vscode-dts/vscode.proposed.diffCommand.d.ts",
|
||||
"../../src/vscode-dts/vscode.proposed.scmActionButton.d.ts",
|
||||
"../../src/vscode-dts/vscode.proposed.scmInput.d.ts",
|
||||
"../../src/vscode-dts/vscode.proposed.scmSelectedProvider.d.ts",
|
||||
"../../src/vscode-dts/vscode.proposed.scmValidation.d.ts",
|
||||
"../../src/vscode-dts/vscode.proposed.tabs.d.ts",
|
||||
|
|
|
@ -37,7 +37,7 @@ export function isSCMResource(element: any): element is ISCMResource {
|
|||
return !!(element as ISCMResource).sourceUri && isSCMResourceGroup((element as ISCMResource).resourceGroup);
|
||||
}
|
||||
|
||||
const compareActions = (a: IAction, b: IAction) => a.id === b.id;
|
||||
const compareActions = (a: IAction, b: IAction) => a.id === b.id && a.enabled === b.enabled;
|
||||
|
||||
export function connectPrimaryMenu(menu: IMenu, callback: (primary: IAction[], secondary: IAction[]) => void, primaryGroup?: string): IDisposable {
|
||||
let cachedDisposable: IDisposable = Disposable.None;
|
||||
|
|
Loading…
Reference in a new issue