Initial implementation (#240264)

This commit is contained in:
Ladislau Szomoru 2025-02-10 18:05:21 +01:00 committed by GitHub
parent 732d06fd55
commit 22c92fc911
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 115 additions and 4 deletions

View File

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

View File

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

View File

@ -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<boolean> {
const config = workspace.getConfiguration('git', Uri.file(repository.root));
const diagnosticSource = config.get<string[]>('diagnosticsCommitHook.Source', []);
const diagnosticSeveritySetting = config.get<DiagnosticSeverityConfig>('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');

View File

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