diff --git a/extensions/git/package.json b/extensions/git/package.json index e0b5cd6cbc5..ec444f5ef0f 100644 --- a/extensions/git/package.json +++ b/extensions/git/package.json @@ -3297,6 +3297,33 @@ "maximum": 40, "markdownDescription": "%config.commitShortHashLength%", "scope": "resource" + }, + "git.diagnosticsCommitHook.Severity": { + "type": "string", + "enum": [ + "error", + "warning", + "information", + "hint" + ], + "enumDescriptions": [ + "%config.diagnosticsCommitHook.Severity.error%", + "%config.diagnosticsCommitHook.Severity.warning%", + "%config.diagnosticsCommitHook.Severity.information%", + "%config.diagnosticsCommitHook.Severity.hint%" + ], + "default": "error", + "markdownDescription": "%config.diagnosticsCommitHook.Severity%", + "scope": "resource" + }, + "git.diagnosticsCommitHook.Source": { + "type": "array", + "items": { + "type": "string" + }, + "default": [], + "markdownDescription": "%config.diagnosticsCommitHook.Source%", + "scope": "resource" } } }, diff --git a/extensions/git/package.nls.json b/extensions/git/package.nls.json index 06b2a6ac32d..b8ba72c17ba 100644 --- a/extensions/git/package.nls.json +++ b/extensions/git/package.nls.json @@ -286,6 +286,12 @@ "config.blameStatusBarItem.enabled": "Controls whether to show blame information in the status bar.", "config.blameStatusBarItem.template": "Template for the blame information status bar item. Supported variables:\n\n* `hash`: Commit hash\n\n* `hashShort`: First N characters of the commit hash according to `#git.commitShortHashLength#`\n\n* `subject`: First line of the commit message\n\n* `authorName`: Author name\n\n* `authorEmail`: Author email\n\n* `authorDate`: Author date\n\n* `authorDateAgo`: Time difference between now and the author date\n\n", "config.commitShortHashLength": "Controls the length of the commit short hash.", + "config.diagnosticsCommitHook.Severity": "Controls the minimum diagnostics severity for which Git should check before committing.", + "config.diagnosticsCommitHook.Severity.error": "Errors only", + "config.diagnosticsCommitHook.Severity.warning": "Errors and warnings", + "config.diagnosticsCommitHook.Severity.information": "Errors, warnings, and information", + "config.diagnosticsCommitHook.Severity.hint": "Errors, warnings, information, and hints", + "config.diagnosticsCommitHook.Source": "Controls the list of diagnostics sources for which Git should check before committing.", "submenu.explorer": "Git", "submenu.commit": "Commit", "submenu.commit.amend": "Amend", diff --git a/extensions/git/src/commands.ts b/extensions/git/src/commands.ts index 60ff8bfa2c7..0cfb2655606 100644 --- a/extensions/git/src/commands.ts +++ b/extensions/git/src/commands.ts @@ -5,7 +5,7 @@ import * as os from 'os'; import * as path from 'path'; -import { Command, commands, Disposable, LineChange, MessageOptions, Position, ProgressLocation, QuickPickItem, Range, SourceControlResourceState, TextDocumentShowOptions, TextEditor, Uri, ViewColumn, window, workspace, WorkspaceEdit, WorkspaceFolder, TimelineItem, env, Selection, TextDocumentContentProvider, InputBoxValidationSeverity, TabInputText, TabInputTextMerge, QuickPickItemKind, TextDocument, LogOutputChannel, l10n, Memento, UIKind, QuickInputButton, ThemeIcon, SourceControlHistoryItem, SourceControl, InputBoxValidationMessage, Tab, TabInputNotebook, QuickInputButtonLocation } from 'vscode'; +import { Command, commands, Disposable, LineChange, MessageOptions, Position, ProgressLocation, QuickPickItem, Range, SourceControlResourceState, TextDocumentShowOptions, TextEditor, Uri, ViewColumn, window, workspace, WorkspaceEdit, WorkspaceFolder, TimelineItem, env, Selection, TextDocumentContentProvider, InputBoxValidationSeverity, TabInputText, TabInputTextMerge, QuickPickItemKind, TextDocument, LogOutputChannel, l10n, Memento, UIKind, QuickInputButton, ThemeIcon, SourceControlHistoryItem, SourceControl, InputBoxValidationMessage, Tab, TabInputNotebook, QuickInputButtonLocation, languages } from 'vscode'; import TelemetryReporter from '@vscode/extension-telemetry'; import { uniqueNamesGenerator, adjectives, animals, colors, NumberDictionary } from '@joaomoreno/unique-names-generator'; import { ForcePushMode, GitErrorCodes, Ref, RefType, Status, CommitOptions, RemoteSourcePublisher, Remote } from './api/git'; @@ -14,7 +14,7 @@ import { Model } from './model'; import { GitResourceGroup, Repository, Resource, ResourceGroupType } from './repository'; import { DiffEditorSelectionHunkToolbarContext, applyLineChanges, getIndexDiffInformation, getModifiedRange, getWorkingTreeDiffInformation, intersectDiffWithRange, invertLineChange, toLineChanges, toLineRanges } from './staging'; import { fromGitUri, toGitUri, isGitUri, toMergeUris, toMultiFileDiffEditorUris } from './uri'; -import { dispose, getCommitShortHash, grep, isDefined, isDescendant, pathEquals, relativePath, truncate } from './util'; +import { DiagnosticSeverityConfig, dispose, getCommitShortHash, grep, isDefined, isDescendant, pathEquals, relativePath, toDiagnosticSeverity, truncate } from './util'; import { GitTimelineItem } from './timelineProvider'; import { ApiRepository } from './api/api1'; import { getRemoteSourceActions, pickRemoteSource } from './remoteSource'; @@ -617,6 +617,66 @@ class CommandErrorOutputTextDocumentContentProvider implements TextDocumentConte } } +async function evaluateDiagnosticsCommitHook(repository: Repository, options: CommitOptions): Promise { + const config = workspace.getConfiguration('git', Uri.file(repository.root)); + const diagnosticSource = config.get('diagnosticsCommitHook.Source', []); + const diagnosticSeveritySetting = config.get('diagnosticsCommitHook.Severity', 'error'); + const diagnosticSeverity = toDiagnosticSeverity(diagnosticSeveritySetting); + + if (diagnosticSource.length === 0) { + return true; + } + + const changes: Uri[] = []; + if (repository.indexGroup.resourceStates.length > 0) { + // Staged files + changes.push(...repository.indexGroup.resourceStates.map(r => r.resourceUri)); + } else if (options.all === 'tracked') { + // Tracked files + changes.push(...repository.workingTreeGroup.resourceStates + .filter(r => r.type !== Status.UNTRACKED && r.type !== Status.IGNORED) + .map(r => r.resourceUri)); + } else { + // All files + changes.push(...repository.workingTreeGroup.resourceStates.map(r => r.resourceUri)); + changes.push(...repository.untrackedGroup.resourceStates.map(r => r.resourceUri)); + } + + const diagnostics = languages.getDiagnostics(); + const changesDiagnostics = diagnostics.filter(([uri, diags]) => + // File + changes.some(u => uri.scheme === 'file' && pathEquals(u.fsPath, uri.fsPath)) && + // Severity + diags.some(d => d.source && diagnosticSource.includes(d.source) && d.severity <= diagnosticSeverity) + ); + + if (changesDiagnostics.length === 0) { + return true; + } + + // Show dialog + const commit = l10n.t('Commit Anyway'); + const view = l10n.t('View Problems'); + + const message = changesDiagnostics.length === 1 + ? l10n.t('The following file has unresolved diagnostic information: {0}.\n\nHow would you like to proceed?', path.basename(changesDiagnostics[0][0].fsPath)) + : l10n.t('There are {0} files that have unresolved diagnostic information.\n\nHow would you like to proceed?', changesDiagnostics.length); + + const choice = await window.showWarningMessage(message, { modal: true }, commit, view); + + // Commit Anyway + if (choice === commit) { + return true; + } + + // View Problems + if (choice === view) { + commands.executeCommand('workbench.panel.markers.view.focus'); + } + + return false; +} + export class CommandCenter { private disposables: Disposable[]; @@ -2273,7 +2333,13 @@ export class CommandCenter { opts.all = 'tracked'; } - // Branch protection + // Diagnostics commit hook + const diagnosticsResult = await evaluateDiagnosticsCommitHook(repository, opts); + if (!diagnosticsResult) { + return; + } + + // Branch protection commit hook const branchProtectionPrompt = config.get<'alwaysCommit' | 'alwaysCommitToNewBranch' | 'alwaysPrompt'>('branchProtectionPrompt')!; if (repository.isBranchProtected() && (branchProtectionPrompt === 'alwaysPrompt' || branchProtectionPrompt === 'alwaysCommitToNewBranch')) { const commitToNewBranch = l10n.t('Commit to a New Branch'); diff --git a/extensions/git/src/util.ts b/extensions/git/src/util.ts index 759ccdf82de..ac2621522f0 100644 --- a/extensions/git/src/util.ts +++ b/extensions/git/src/util.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Event, Disposable, EventEmitter, SourceControlHistoryItemRef, l10n, workspace, Uri } from 'vscode'; +import { Event, Disposable, EventEmitter, SourceControlHistoryItemRef, l10n, workspace, Uri, DiagnosticSeverity } from 'vscode'; import { dirname, sep, relative } from 'path'; import { Readable } from 'stream'; import { promises as fs, createReadStream } from 'fs'; @@ -772,3 +772,15 @@ export function getCommitShortHash(scope: Uri, hash: string): string { const shortHashLength = config.get('commitShortHashLength', 7); return hash.substring(0, shortHashLength); } + +export type DiagnosticSeverityConfig = 'error' | 'warning' | 'information' | 'hint'; + +export function toDiagnosticSeverity(value: DiagnosticSeverityConfig): DiagnosticSeverity { + return value === 'error' + ? DiagnosticSeverity.Error + : value === 'warning' + ? DiagnosticSeverity.Warning + : value === 'information' + ? DiagnosticSeverity.Information + : DiagnosticSeverity.Hint; +}