Skip to content

Commit 6f5fc17

Browse files
authored
Git - use editor as commit message input (microsoft#151491)
1 parent 793b0fd commit 6f5fc17

15 files changed

+229
-45
lines changed

extensions/git/extension.webpack.config.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ module.exports = withDefaults({
1313
context: __dirname,
1414
entry: {
1515
main: './src/main.ts',
16-
['askpass-main']: './src/askpass-main.ts'
16+
['askpass-main']: './src/askpass-main.ts',
17+
['git-editor-main']: './src/git-editor-main.ts'
1718
}
1819
});

extensions/git/package.json

+45-16
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
"contribMergeEditorToolbar",
1515
"contribViewsWelcome",
1616
"scmActionButton",
17+
"scmInput",
1718
"scmSelectedProvider",
1819
"scmValidation",
1920
"timeline"
@@ -213,83 +214,99 @@
213214
"command": "git.commit",
214215
"title": "%command.commit%",
215216
"category": "Git",
216-
"icon": "$(check)"
217+
"icon": "$(check)",
218+
"enablement": "!commitInProgress"
217219
},
218220
{
219221
"command": "git.commitStaged",
220222
"title": "%command.commitStaged%",
221-
"category": "Git"
223+
"category": "Git",
224+
"enablement": "!commitInProgress"
222225
},
223226
{
224227
"command": "git.commitEmpty",
225228
"title": "%command.commitEmpty%",
226-
"category": "Git"
229+
"category": "Git",
230+
"enablement": "!commitInProgress"
227231
},
228232
{
229233
"command": "git.commitStagedSigned",
230234
"title": "%command.commitStagedSigned%",
231-
"category": "Git"
235+
"category": "Git",
236+
"enablement": "!commitInProgress"
232237
},
233238
{
234239
"command": "git.commitStagedAmend",
235240
"title": "%command.commitStagedAmend%",
236-
"category": "Git"
241+
"category": "Git",
242+
"enablement": "!commitInProgress"
237243
},
238244
{
239245
"command": "git.commitAll",
240246
"title": "%command.commitAll%",
241-
"category": "Git"
247+
"category": "Git",
248+
"enablement": "!commitInProgress"
242249
},
243250
{
244251
"command": "git.commitAllSigned",
245252
"title": "%command.commitAllSigned%",
246-
"category": "Git"
253+
"category": "Git",
254+
"enablement": "!commitInProgress"
247255
},
248256
{
249257
"command": "git.commitAllAmend",
250258
"title": "%command.commitAllAmend%",
251-
"category": "Git"
259+
"category": "Git",
260+
"enablement": "!commitInProgress"
252261
},
253262
{
254263
"command": "git.commitNoVerify",
255264
"title": "%command.commitNoVerify%",
256265
"category": "Git",
257-
"icon": "$(check)"
266+
"icon": "$(check)",
267+
"enablement": "!commitInProgress"
258268
},
259269
{
260270
"command": "git.commitStagedNoVerify",
261271
"title": "%command.commitStagedNoVerify%",
262-
"category": "Git"
272+
"category": "Git",
273+
"enablement": "!commitInProgress"
263274
},
264275
{
265276
"command": "git.commitEmptyNoVerify",
266277
"title": "%command.commitEmptyNoVerify%",
267-
"category": "Git"
278+
"category": "Git",
279+
"enablement": "!commitInProgress"
268280
},
269281
{
270282
"command": "git.commitStagedSignedNoVerify",
271283
"title": "%command.commitStagedSignedNoVerify%",
272-
"category": "Git"
284+
"category": "Git",
285+
"enablement": "!commitInProgress"
273286
},
274287
{
275288
"command": "git.commitStagedAmendNoVerify",
276289
"title": "%command.commitStagedAmendNoVerify%",
277-
"category": "Git"
290+
"category": "Git",
291+
"enablement": "!commitInProgress"
278292
},
279293
{
280294
"command": "git.commitAllNoVerify",
281295
"title": "%command.commitAllNoVerify%",
282-
"category": "Git"
296+
"category": "Git",
297+
"enablement": "!commitInProgress"
283298
},
284299
{
285300
"command": "git.commitAllSignedNoVerify",
286301
"title": "%command.commitAllSignedNoVerify%",
287-
"category": "Git"
302+
"category": "Git",
303+
"enablement": "!commitInProgress"
288304
},
289305
{
290306
"command": "git.commitAllAmendNoVerify",
291307
"title": "%command.commitAllAmendNoVerify%",
292-
"category": "Git"
308+
"category": "Git",
309+
"enablement": "!commitInProgress"
293310
},
294311
{
295312
"command": "git.restoreCommitTemplate",
@@ -2013,6 +2030,18 @@
20132030
"scope": "machine",
20142031
"description": "%config.defaultCloneDirectory%"
20152032
},
2033+
"git.useEditorAsCommitInput": {
2034+
"type": "boolean",
2035+
"scope": "resource",
2036+
"description": "%config.useEditorAsCommitInput%",
2037+
"default": false
2038+
},
2039+
"git.verboseCommit": {
2040+
"type": "boolean",
2041+
"scope": "resource",
2042+
"markdownDescription": "%config.verboseCommit%",
2043+
"default": false
2044+
},
20162045
"git.enableSmartCommit": {
20172046
"type": "boolean",
20182047
"scope": "resource",

extensions/git/package.nls.json

+2
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,8 @@
140140
"config.ignoreLimitWarning": "Ignores the warning when there are too many changes in a repository.",
141141
"config.ignoreRebaseWarning": "Ignores the warning when it looks like the branch might have been rebased when pulling.",
142142
"config.defaultCloneDirectory": "The default location to clone a git repository.",
143+
"config.useEditorAsCommitInput": "Use an editor to author the commit message.",
144+
"config.verboseCommit": "Enable verbose output when `#git.useEditorAsCommitInput#` is enabled.",
143145
"config.enableSmartCommit": "Commit all changes when there are no staged changes.",
144146
"config.smartCommitChanges": "Control which changes are automatically staged by Smart Commit.",
145147
"config.smartCommitChanges.all": "Automatically stage all changes.",

extensions/git/src/api/git.d.ts

+3
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,8 @@ export interface CommitOptions {
137137
empty?: boolean;
138138
noVerify?: boolean;
139139
requireUserConfig?: boolean;
140+
useEditor?: boolean;
141+
verbose?: boolean;
140142
}
141143

142144
export interface FetchOptions {
@@ -336,4 +338,5 @@ export const enum GitErrorCodes {
336338
PatchDoesNotApply = 'PatchDoesNotApply',
337339
NoPathFound = 'NoPathFound',
338340
UnknownPath = 'UnknownPath',
341+
EmptyCommitMessage = 'EmptyCommitMessage'
339342
}

extensions/git/src/askpass.ts

+2-12
Original file line numberDiff line numberDiff line change
@@ -6,26 +6,16 @@
66
import { window, InputBoxOptions, Uri, Disposable, workspace } from 'vscode';
77
import { IDisposable, EmptyDisposable, toDisposable } from './util';
88
import * as path from 'path';
9-
import { IIPCHandler, IIPCServer, createIPCServer } from './ipc/ipcServer';
9+
import { IIPCHandler, IIPCServer } from './ipc/ipcServer';
1010
import { CredentialsProvider, Credentials } from './api/git';
11-
import { OutputChannelLogger } from './log';
1211

1312
export class Askpass implements IIPCHandler {
1413

1514
private disposable: IDisposable = EmptyDisposable;
1615
private cache = new Map<string, Credentials>();
1716
private credentialsProviders = new Set<CredentialsProvider>();
1817

19-
static async create(outputChannelLogger: OutputChannelLogger, context?: string): Promise<Askpass> {
20-
try {
21-
return new Askpass(await createIPCServer(context));
22-
} catch (err) {
23-
outputChannelLogger.logError(`Failed to create git askpass IPC: ${err}`);
24-
return new Askpass();
25-
}
26-
}
27-
28-
private constructor(private ipc?: IIPCServer) {
18+
constructor(private ipc?: IIPCServer) {
2919
if (ipc) {
3020
this.disposable = ipc.registerHandler('askpass', this);
3121
}

extensions/git/src/commands.ts

+33-6
Original file line numberDiff line numberDiff line change
@@ -1516,6 +1516,14 @@ export class CommandCenter {
15161516
opts.signoff = true;
15171517
}
15181518

1519+
if (config.get<boolean>('useEditorAsCommitInput')) {
1520+
opts.useEditor = true;
1521+
1522+
if (config.get<boolean>('verboseCommit')) {
1523+
opts.verbose = true;
1524+
}
1525+
}
1526+
15191527
const smartCommitChanges = config.get<'all' | 'tracked'>('smartCommitChanges');
15201528

15211529
if (
@@ -1563,7 +1571,7 @@ export class CommandCenter {
15631571

15641572
let message = await getCommitMessage();
15651573

1566-
if (!message && !opts.amend) {
1574+
if (!message && !opts.amend && !opts.useEditor) {
15671575
return false;
15681576
}
15691577

@@ -1623,10 +1631,13 @@ export class CommandCenter {
16231631

16241632
private async commitWithAnyInput(repository: Repository, opts?: CommitOptions): Promise<void> {
16251633
const message = repository.inputBox.value;
1634+
const root = Uri.file(repository.root);
1635+
const config = workspace.getConfiguration('git', root);
1636+
16261637
const getCommitMessage = async () => {
16271638
let _message: string | undefined = message;
16281639

1629-
if (!_message) {
1640+
if (!_message && !config.get<boolean>('useEditorAsCommitInput')) {
16301641
let value: string | undefined = undefined;
16311642

16321643
if (opts && opts.amend && repository.HEAD && repository.HEAD.commit) {
@@ -3010,7 +3021,7 @@ export class CommandCenter {
30103021
};
30113022

30123023
let message: string;
3013-
let type: 'error' | 'warning' = 'error';
3024+
let type: 'error' | 'warning' | 'information' = 'error';
30143025

30153026
const choices = new Map<string, () => void>();
30163027
const openOutputChannelChoice = localize('open git log', "Open Git Log");
@@ -3073,6 +3084,12 @@ export class CommandCenter {
30733084
message = localize('missing user info', "Make sure you configure your 'user.name' and 'user.email' in git.");
30743085
choices.set(localize('learn more', "Learn More"), () => commands.executeCommand('vscode.open', Uri.parse('https://aka.ms/vscode-setup-git')));
30753086
break;
3087+
case GitErrorCodes.EmptyCommitMessage:
3088+
message = localize('empty commit', "Commit operation was cancelled due to empty commit message.");
3089+
choices.clear();
3090+
type = 'information';
3091+
options.modal = false;
3092+
break;
30763093
default: {
30773094
const hint = (err.stderr || err.message || String(err))
30783095
.replace(/^error: /mi, '')
@@ -3094,10 +3111,20 @@ export class CommandCenter {
30943111
return;
30953112
}
30963113

3114+
let result: string | undefined;
30973115
const allChoices = Array.from(choices.keys());
3098-
const result = type === 'error'
3099-
? await window.showErrorMessage(message, options, ...allChoices)
3100-
: await window.showWarningMessage(message, options, ...allChoices);
3116+
3117+
switch (type) {
3118+
case 'error':
3119+
result = await window.showErrorMessage(message, options, ...allChoices);
3120+
break;
3121+
case 'warning':
3122+
result = await window.showWarningMessage(message, options, ...allChoices);
3123+
break;
3124+
case 'information':
3125+
result = await window.showInformationMessage(message, options, ...allChoices);
3126+
break;
3127+
}
31013128

31023129
if (result) {
31033130
const resultFn = choices.get(result);
+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
#!/bin/sh

extensions/git/src/git-editor-main.ts

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
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+
import { IPCClient } from './ipc/ipcClient';
6+
7+
function fatal(err: any): void {
8+
console.error(err);
9+
process.exit(1);
10+
}
11+
12+
function main(argv: string[]): void {
13+
const ipcClient = new IPCClient('git-editor');
14+
const commitMessagePath = argv[argv.length - 1];
15+
16+
ipcClient.call({ commitMessagePath }).then(() => {
17+
setTimeout(() => process.exit(0), 0);
18+
}).catch(err => fatal(err));
19+
}
20+
21+
main(process.argv);

extensions/git/src/git-editor.sh

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
#!/bin/sh
2+
3+
ELECTRON_RUN_AS_NODE="1" \
4+
"$VSCODE_GIT_EDITOR_NODE" "$VSCODE_GIT_EDITOR_MAIN" $VSCODE_GIT_EDITOR_EXTRA_ARGS $@

extensions/git/src/git.ts

+27-7
Original file line numberDiff line numberDiff line change
@@ -1400,20 +1400,37 @@ export class Repository {
14001400
}
14011401

14021402
async commit(message: string | undefined, opts: CommitOptions = Object.create(null)): Promise<void> {
1403-
const args = ['commit', '--quiet', '--allow-empty-message'];
1403+
const args = ['commit', '--quiet'];
1404+
const options: SpawnOptions = {};
1405+
1406+
if (message) {
1407+
options.input = message;
1408+
args.push('--file', '-');
1409+
}
1410+
1411+
if (opts.verbose) {
1412+
args.push('--verbose');
1413+
}
14041414

14051415
if (opts.all) {
14061416
args.push('--all');
14071417
}
14081418

1409-
if (opts.amend && message) {
1419+
if (opts.amend) {
14101420
args.push('--amend');
14111421
}
14121422

1413-
if (opts.amend && !message) {
1414-
args.push('--amend', '--no-edit');
1415-
} else {
1416-
args.push('--file', '-');
1423+
if (!opts.useEditor) {
1424+
if (!message) {
1425+
if (opts.amend) {
1426+
args.push('--no-edit');
1427+
} else {
1428+
options.input = '';
1429+
args.push('--file', '-');
1430+
}
1431+
}
1432+
1433+
args.push('--allow-empty-message');
14171434
}
14181435

14191436
if (opts.signoff) {
@@ -1438,7 +1455,7 @@ export class Repository {
14381455
}
14391456

14401457
try {
1441-
await this.exec(args, !opts.amend || message ? { input: message || '' } : {});
1458+
await this.exec(args, options);
14421459
} catch (commitErr) {
14431460
await this.handleCommitError(commitErr);
14441461
}
@@ -1462,6 +1479,9 @@ export class Repository {
14621479
if (/not possible because you have unmerged files/.test(commitErr.stderr || '')) {
14631480
commitErr.gitErrorCode = GitErrorCodes.UnmergedChanges;
14641481
throw commitErr;
1482+
} else if (/Aborting commit due to empty commit message/.test(commitErr.stderr || '')) {
1483+
commitErr.gitErrorCode = GitErrorCodes.EmptyCommitMessage;
1484+
throw commitErr;
14651485
}
14661486

14671487
try {

0 commit comments

Comments
 (0)