Merge pull request #558 from steaks/commit-prompt

Added Generate Commit Message button for git
This commit is contained in:
Andrew Pareles 2025-06-05 19:28:41 -07:00 committed by GitHub
commit 7b14f0ca73
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 484 additions and 5 deletions

View file

@ -130,8 +130,9 @@ import { IVoidUpdateService } from '../../workbench/contrib/void/common/voidUpda
import { MetricsMainService } from '../../workbench/contrib/void/electron-main/metricsMainService.js';
import { VoidMainUpdateService } from '../../workbench/contrib/void/electron-main/voidUpdateMainService.js';
import { LLMMessageChannel } from '../../workbench/contrib/void/electron-main/sendLLMMessageChannel.js';
import { VoidSCMService } from '../../workbench/contrib/void/electron-main/voidSCMMainService.js';
import { IVoidSCMService } from '../../workbench/contrib/void/common/voidSCMTypes.js';
import { MCPChannel } from '../../workbench/contrib/void/electron-main/mcpChannel.js';
/**
* The main VS Code application. There will only ever be one instance,
* even if the user starts many instances (e.g. from the command line).
@ -1103,6 +1104,7 @@ export class CodeApplication extends Disposable {
// Void main process services (required for services with a channel for comm between browser and electron-main (node))
services.set(IMetricsService, new SyncDescriptor(MetricsMainService, undefined, false));
services.set(IVoidUpdateService, new SyncDescriptor(VoidMainUpdateService, undefined, false));
services.set(IVoidSCMService, new SyncDescriptor(VoidSCMService, undefined, false));
// Default Extensions Profile Init
services.set(IExtensionsProfileScannerService, new SyncDescriptor(ExtensionsProfileScannerService, undefined, true));
@ -1244,6 +1246,11 @@ export class CodeApplication extends Disposable {
const sendLLMMessageChannel = new LLMMessageChannel(accessor.get(IMetricsService));
mainProcessElectronServer.registerChannel('void-channel-llmMessage', sendLLMMessageChannel);
// Void added this
const voidSCMChannel = ProxyChannel.fromService(accessor.get(IVoidSCMService), disposables);
mainProcessElectronServer.registerChannel('void-channel-scm', voidSCMChannel);
// Void added this
const mcpChannel = new MCPChannel();
mainProcessElectronServer.registerChannel('void-channel-mcp', mcpChannel);

View file

@ -123,6 +123,7 @@ const featureNameMap: { display: string, featureName: FeatureName }[] = [
{ display: 'Quick Edit', featureName: 'Ctrl+K' },
{ display: 'Autocomplete', featureName: 'Autocomplete' },
{ display: 'Fast Apply', featureName: 'Apply' },
{ display: 'Source Control', featureName: 'SCM' },
];
const AddProvidersPage = ({ pageIndex, setPageIndex }: { pageIndex: number, setPageIndex: (index: number) => void }) => {

View file

@ -688,6 +688,7 @@ const ProviderSetting = ({ providerName, settingName, subTextMd }: { providerNam
// </div >
// }
export const SettingsForProvider = ({ providerName, showProviderTitle, showProviderSuggestions }: { providerName: ProviderName, showProviderTitle: boolean, showProviderSuggestions: boolean }) => {
const voidSettingsState = useSettingsState()
@ -1341,6 +1342,33 @@ export const Settings = () => {
</ErrorBoundary>
</div>
</div>
{/* SCM */}
<ErrorBoundary>
<div className='w-full'>
<h4 className={`text-base`}>{displayInfoOfFeatureName('SCM')}</h4>
<div className='text-sm italic text-void-fg-3 mt-1'>Settings that control the behavior of the commit message generator.</div>
<div className='my-2'>
{/* Sync to Chat Switch */}
<div className='flex items-center gap-x-2 my-2'>
<VoidSwitch
size='xs'
value={settingsState.globalSettings.syncSCMToChat}
onChange={(newVal) => voidSettingsService.setGlobalSetting('syncSCMToChat', newVal)}
/>
<span className='text-void-fg-3 text-xs pointer-events-none'>{settingsState.globalSettings.syncSCMToChat ? 'Same as Chat model' : 'Different model'}</span>
</div>
{/* Model Dropdown */}
<div className={`my-2 ${settingsState.globalSettings.syncSCMToChat ? 'hidden' : ''}`}>
<ModelDropdown featureName={'SCM'} className='text-xs text-void-fg-3 bg-void-bg-1 border border-void-border-1 rounded p-0.5 px-1' />
</div>
</div>
</div>
</ErrorBoundary>
</div>
</ErrorBoundary>
</div>

View file

@ -61,6 +61,9 @@ import './miscWokrbenchContrib.js'
// register file service (for explorer context menu)
import './fileService.js'
// register source control management
import './voidSCMService.js'
// ---------- common (unclear if these actually need to be imported, because they're already imported wherever they're used) ----------
// llmMessage

View file

@ -0,0 +1,232 @@
import { ThemeIcon } from '../../../../base/common/themables.js'
import { localize2 } from '../../../../nls.js'
import { Action2, MenuId, registerAction2 } from '../../../../platform/actions/common/actions.js'
import { ContextKeyExpr, IContextKey, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'
import { ISCMService } from '../../scm/common/scm.js'
import { ProxyChannel } from '../../../../base/parts/ipc/common/ipc.js'
import { IVoidSCMService } from '../common/voidSCMTypes.js'
import { IMainProcessService } from '../../../../platform/ipc/common/mainProcessService.js'
import { IVoidSettingsService } from '../common/voidSettingsService.js'
import { IConvertToLLMMessageService } from './convertToLLMMessageService.js'
import { ILLMMessageService } from '../common/sendLLMMessageService.js'
import { ModelSelection, OverridesOfModel, ModelSelectionOptions } from '../common/voidSettingsTypes.js'
import { gitCommitMessage_systemMessage, gitCommitMessage_userMessage } from '../common/prompt/prompts.js'
import { LLMChatMessage } from '../common/sendLLMMessageTypes.js'
import { generateUuid } from '../../../../base/common/uuid.js'
import { ThrottledDelayer } from '../../../../base/common/async.js'
import { CancellationError, isCancellationError } from '../../../../base/common/errors.js'
import { registerSingleton, InstantiationType } from '../../../../platform/instantiation/common/extensions.js'
import { createDecorator, ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js'
import { Disposable } from '../../../../base/common/lifecycle.js'
import { INotificationService } from '../../../../platform/notification/common/notification.js'
// this is OK, it's just a type
import type { ISCMRepository } from '../../scm/common/scm.js'
interface ModelOptions {
modelSelection: ModelSelection | null
modelSelectionOptions?: ModelSelectionOptions
overridesOfModel: OverridesOfModel
}
export interface IGenerateCommitMessageService {
readonly _serviceBrand: undefined
generateCommitMessage(): Promise<void>
abort(): void
}
export const IGenerateCommitMessageService = createDecorator<IGenerateCommitMessageService>('voidGenerateCommitMessageService');
const loadingContextKey = 'voidSCMGenerateCommitMessageLoading'
class GenerateCommitMessageService extends Disposable implements IGenerateCommitMessageService {
readonly _serviceBrand: undefined;
private readonly execute = new ThrottledDelayer(300)
private llmRequestId: string | null = null
private currentRequestId: string | null = null
private voidSCM: IVoidSCMService
private loadingContextKey: IContextKey<boolean>
constructor(
@ISCMService private readonly scmService: ISCMService,
@IMainProcessService mainProcessService: IMainProcessService,
@IVoidSettingsService private readonly voidSettingsService: IVoidSettingsService,
@IConvertToLLMMessageService private readonly convertToLLMMessageService: IConvertToLLMMessageService,
@ILLMMessageService private readonly llmMessageService: ILLMMessageService,
@IContextKeyService private readonly contextKeyService: IContextKeyService,
@INotificationService private readonly notificationService: INotificationService
) {
super()
this.loadingContextKey = this.contextKeyService.createKey(loadingContextKey, false)
this.voidSCM = ProxyChannel.toService<IVoidSCMService>(mainProcessService.getChannel('void-channel-scm'));
}
override dispose() {
this.execute.dispose()
super.dispose()
}
async generateCommitMessage() {
this.loadingContextKey.set(true)
this.execute.trigger(async () => {
const requestId = generateUuid()
this.currentRequestId = requestId
try {
const { path, repo } = this.gitRepoInfo()
const [stat, sampledDiffs, branch, log] = await Promise.all([
this.voidSCM.gitStat(path),
this.voidSCM.gitSampledDiffs(path),
this.voidSCM.gitBranch(path),
this.voidSCM.gitLog(path)
])
if (!this.isCurrentRequest(requestId)) { throw new CancellationError() }
const modelSelection = this.voidSettingsService.state.modelSelectionOfFeature['SCM'] ?? null
const modelSelectionOptions = modelSelection ? this.voidSettingsService.state.optionsOfModelSelection['SCM'][modelSelection?.providerName]?.[modelSelection.modelName] : undefined
const overridesOfModel = this.voidSettingsService.state.overridesOfModel
const modelOptions: ModelOptions = { modelSelection, modelSelectionOptions, overridesOfModel }
const prompt = gitCommitMessage_userMessage(stat, sampledDiffs, branch, log)
const simpleMessages = [{ role: 'user', content: prompt } as const]
const { messages, separateSystemMessage } = this.convertToLLMMessageService.prepareLLMSimpleMessages({
simpleMessages,
systemMessage: gitCommitMessage_systemMessage,
modelSelection: modelOptions.modelSelection,
featureName: 'SCM',
})
const commitMessage = await this.sendLLMMessage(messages, separateSystemMessage!, modelOptions)
if (!this.isCurrentRequest(requestId)) { throw new CancellationError() }
this.setCommitMessage(repo, commitMessage)
} catch (error) {
this.onError(error)
} finally {
if (this.isCurrentRequest(requestId)) {
this.loadingContextKey.set(false)
}
}
})
}
abort() {
if (this.llmRequestId) {
this.llmMessageService.abort(this.llmRequestId)
}
this.execute.cancel()
this.loadingContextKey.set(false)
this.currentRequestId = null
}
private gitRepoInfo() {
const repo = Array.from(this.scmService.repositories || []).find((r: any) => r.provider.contextValue === 'git')
if (!repo) { throw new Error('No git repository found') }
if (!repo.provider.rootUri?.fsPath) { throw new Error('No git repository root path found') }
return { path: repo.provider.rootUri.fsPath, repo }
}
/** LLM Functions */
private sendLLMMessage(messages: LLMChatMessage[], separateSystemMessage: string, modelOptions: ModelOptions): Promise<string> {
return new Promise((resolve, reject) => {
this.llmRequestId = this.llmMessageService.sendLLMMessage({
messagesType: 'chatMessages',
messages,
separateSystemMessage,
chatMode: null,
modelSelection: modelOptions.modelSelection,
modelSelectionOptions: modelOptions.modelSelectionOptions,
overridesOfModel: modelOptions.overridesOfModel,
onText: () => { },
onFinalMessage: (params: { fullText: string }) => {
const match = params.fullText.match(/<output>([\s\S]*?)<\/output>/i)
const commitMessage = match ? match[1].trim() : ''
resolve(commitMessage)
},
onError: (error) => {
console.error(error)
reject(error)
},
onAbort: () => {
reject(new CancellationError())
},
logging: { loggingName: 'VoidSCM - Commit Message' },
})
})
}
/** Request Helpers */
private isCurrentRequest(requestId: string) {
return requestId === this.currentRequestId
}
/** UI Functions */
private setCommitMessage(repo: ISCMRepository, commitMessage: string) {
repo.input.setValue(commitMessage, false)
}
private onError(error: any) {
if (!isCancellationError(error)) {
console.error(error)
this.notificationService.error(localize2('voidFailedToGenerateCommitMessage', 'Failed to generate commit message.').value)
}
}
}
class GenerateCommitMessageAction extends Action2 {
constructor() {
super({
id: 'void.generateCommitMessageAction',
title: localize2('voidCommitMessagePrompt', 'Void: Generate Commit Message'),
icon: ThemeIcon.fromId('sparkle'),
tooltip: localize2('voidCommitMessagePromptTooltip', 'Void: Generate Commit Message'),
f1: true,
menu: [{
id: MenuId.SCMInputBox,
when: ContextKeyExpr.and(ContextKeyExpr.equals('scmProvider', 'git'), ContextKeyExpr.equals(loadingContextKey, false)),
group: 'inline'
}]
})
}
async run(accessor: ServicesAccessor): Promise<void> {
const generateCommitMessageService = accessor.get(IGenerateCommitMessageService)
generateCommitMessageService.generateCommitMessage()
}
}
class LoadingGenerateCommitMessageAction extends Action2 {
constructor() {
super({
id: 'void.loadingGenerateCommitMessageAction',
title: localize2('voidCommitMessagePromptCancel', 'Void: Cancel Commit Message Generation'),
icon: ThemeIcon.fromId('stop-circle'),
tooltip: localize2('voidCommitMessagePromptCancelTooltip', 'Void: Cancel Commit Message Generation'),
f1: false, //Having a cancel command in the command palette is more confusing than useful.
menu: [{
id: MenuId.SCMInputBox,
when: ContextKeyExpr.and(ContextKeyExpr.equals('scmProvider', 'git'), ContextKeyExpr.equals(loadingContextKey, true)),
group: 'inline'
}]
})
}
async run(accessor: ServicesAccessor): Promise<void> {
const generateCommitMessageService = accessor.get(IGenerateCommitMessageService)
generateCommitMessageService.abort()
}
}
registerSingleton(IGenerateCommitMessageService, GenerateCommitMessageService, InstantiationType.Delayed)
registerAction2(GenerateCommitMessageAction)
registerAction2(LoadingGenerateCommitMessageAction)

View file

@ -980,3 +980,77 @@ Store Result: After computing fib(n), the result is stored in memo for future re
## END EXAMPLES
*/
// ======================================================== scm ========================================================================
export const gitCommitMessage_systemMessage = `
You are an expert software engineer AI assistant responsible for writing clear and concise Git commit messages that summarize the **purpose** and **intent** of the change. Try to keep your commit messages to one sentence. If necessary, you can use two sentences.
You always respond with:
- The commit message wrapped in <output> tags
- A brief explanation of the reasoning behind the message, wrapped in <reasoning> tags
Example format:
<output>Fix login bug and improve error handling</output>
<reasoning>This commit updates the login handler to fix a redirect issue and improves frontend error messages for failed logins.</reasoning>
Do not include anything else outside of these tags.
Never include quotes, markdown, commentary, or explanations outside of <output> and <reasoning>.`.trim()
/**
* Create a user message for the LLM to generate a commit message. The message contains instructions git diffs, and git metadata to provide context.
*
* @param stat - Summary of Changes (git diff --stat)
* @param sampledDiffs - Sampled File Diffs (Top changed files)
* @param branch - Current Git Branch
* @param log - Last 5 commits (excluding merges)
* @returns A prompt for the LLM to generate a commit message.
*
* @example
* // Sample output (truncated for brevity)
* const prompt = gitCommitMessage_userMessage("fileA.ts | 10 ++--", "diff --git a/fileA.ts...", "main", "abc123|Fix bug|2025-01-01\n...")
*
* // Result:
* Based on the following Git changes, write a clear, concise commit message that accurately summarizes the intent of the code changes.
*
* Section 1 - Summary of Changes (git diff --stat):
* fileA.ts | 10 ++--
*
* Section 2 - Sampled File Diffs (Top changed files):
* diff --git a/fileA.ts b/fileA.ts
* ...
*
* Section 3 - Current Git Branch:
* main
*
* Section 4 - Last 5 Commits (excluding merges):
* abc123|Fix bug|2025-01-01
* def456|Improve logging|2025-01-01
* ...
*/
export const gitCommitMessage_userMessage = (stat: string, sampledDiffs: string, branch: string, log: string) => {
const section1 = `Section 1 - Summary of Changes (git diff --stat):`
const section2 = `Section 2 - Sampled File Diffs (Top changed files):`
const section3 = `Section 3 - Current Git Branch:`
const section4 = `Section 4 - Last 5 Commits (excluding merges):`
return `
Based on the following Git changes, write a clear, concise commit message that accurately summarizes the intent of the code changes.
${section1}
${stat}
${section2}
${sampledDiffs}
${section3}
${branch}
${section4}
${log}`.trim()
}

View file

@ -0,0 +1,31 @@
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
export interface IVoidSCMService {
readonly _serviceBrand: undefined;
/**
* Get git diff --stat
*
* @param path Path to the git repository
*/
gitStat(path: string): Promise<string>
/**
* Get git diff --stat for the top 10 most significantly changed files according to lines added/removed
*
* @param path Path to the git repository
*/
gitSampledDiffs(path: string): Promise<string>
/**
* Get the current git branch
*
* @param path Path to the git repository
*/
gitBranch(path: string): Promise<string>
/**
* Get the last 5 commits excluding merges
*
* @param path Path to the git repository
*/
gitLog(path: string): Promise<string>
}
export const IVoidSCMService = createDecorator<IVoidSCMService>('voidSCMService')

View file

@ -116,6 +116,7 @@ export const modelFilterOfFeatureName: {
'Chat': { filter: o => true, emptyMessage: null, },
'Ctrl+K': { filter: o => true, emptyMessage: null, },
'Apply': { filter: o => true, emptyMessage: null, },
'SCM': { filter: o => true, emptyMessage: null, },
}
@ -213,9 +214,9 @@ const _validatedModelState = (state: Omit<VoidSettingsState, '_modelOptions'>):
const defaultState = () => {
const d: VoidSettingsState = {
settingsOfProvider: deepClone(defaultSettingsOfProvider),
modelSelectionOfFeature: { 'Chat': null, 'Ctrl+K': null, 'Autocomplete': null, 'Apply': null },
modelSelectionOfFeature: { 'Chat': null, 'Ctrl+K': null, 'Autocomplete': null, 'Apply': null, 'SCM': null },
globalSettings: deepClone(defaultGlobalSettings),
optionsOfModelSelection: { 'Chat': {}, 'Ctrl+K': {}, 'Autocomplete': {}, 'Apply': {} },
optionsOfModelSelection: { 'Chat': {}, 'Ctrl+K': {}, 'Autocomplete': {}, 'Apply': {}, 'SCM': {} },
overridesOfModel: deepClone(defaultOverridesOfModel),
_modelOptions: [], // computed later
mcpUserStateOfName: {},
@ -262,6 +263,7 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService {
await this._storeState()
this._onDidChangeState.fire()
this._onUpdate_syncApplyToChat()
this._onUpdate_syncSCMToChat()
}
async resetState() {
await this.dangerousSetState(defaultState())
@ -280,6 +282,12 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService {
// autoapprove is now an obj not a boolean (1.2.5)
if (typeof readS.globalSettings.autoApprove === 'boolean') readS.globalSettings.autoApprove = {}
// 1.3.5 add source control feature
if (readS.modelSelectionOfFeature && !readS.modelSelectionOfFeature['SCM']) {
readS.modelSelectionOfFeature['SCM'] = deepClone(readS.modelSelectionOfFeature['Chat'])
readS.optionsOfModelSelection['SCM'] = deepClone(readS.optionsOfModelSelection['Chat'])
}
// add disableSystemMessage feature
if (readS.globalSettings.disableSystemMessage === undefined) readS.globalSettings.disableSystemMessage = false;
}
catch (e) {
@ -392,7 +400,10 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService {
private _onUpdate_syncApplyToChat() {
// if sync is turned on, sync (call this whenever Chat model or !!sync changes)
this.setModelSelectionOfFeature('Apply', deepClone(this.state.modelSelectionOfFeature['Chat']))
}
private _onUpdate_syncSCMToChat() {
this.setModelSelectionOfFeature('SCM', deepClone(this.state.modelSelectionOfFeature['Chat']))
}
setGlobalSetting: SetGlobalSettingFn = async (settingName, newVal) => {
@ -409,6 +420,8 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService {
// hooks
if (this.state.globalSettings.syncApplyToChat) this._onUpdate_syncApplyToChat()
if (this.state.globalSettings.syncSCMToChat) this._onUpdate_syncSCMToChat()
}
@ -428,7 +441,9 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService {
// hooks
if (featureName === 'Chat') {
if (this.state.globalSettings.syncApplyToChat) this._onUpdate_syncApplyToChat()
// When Chat model changes, update synced features
this._onUpdate_syncApplyToChat()
this._onUpdate_syncSCMToChat()
}
}

View file

@ -365,7 +365,7 @@ export const modelSelectionsEqual = (m1: ModelSelection, m2: ModelSelection) =>
}
// this is a state
export const featureNames = ['Chat', 'Ctrl+K', 'Autocomplete', 'Apply'] as const
export const featureNames = ['Chat', 'Ctrl+K', 'Autocomplete', 'Apply', 'SCM'] as const
export type ModelSelectionOfFeature = Record<(typeof featureNames)[number], ModelSelection | null>
export type FeatureName = keyof ModelSelectionOfFeature
@ -380,6 +380,9 @@ export const displayInfoOfFeatureName = (featureName: FeatureName) => {
return 'Chat'
else if (featureName === 'Apply')
return 'Apply'
// source control:
else if (featureName === 'SCM')
return 'Commit Message Generator'
else
throw new Error(`Feature Name ${featureName} not allowed`)
}
@ -443,6 +446,7 @@ export type GlobalSettings = {
aiInstructions: string;
enableAutocomplete: boolean;
syncApplyToChat: boolean;
syncSCMToChat: boolean;
enableFastApply: boolean;
chatMode: ChatMode;
autoApprove: { [approvalType in ToolApprovalType]?: boolean };
@ -457,6 +461,7 @@ export const defaultGlobalSettings: GlobalSettings = {
aiInstructions: '',
enableAutocomplete: false,
syncApplyToChat: true,
syncSCMToChat: true,
enableFastApply: true,
chatMode: 'agent',
autoApprove: {},

View file

@ -563,8 +563,11 @@ const sendAnthropicChat = async ({ messages, providerName, onText, onFinalMessag
stream.on('finalMessage', (response) => {
const anthropicReasoning = response.content.filter(c => c.type === 'thinking' || c.type === 'redacted_thinking')
const tools = response.content.filter(c => c.type === 'tool_use')
// console.log('TOOLS!!!!!!', JSON.stringify(tools, null, 2))
// console.log('TOOLS!!!!!!', JSON.stringify(response, null, 2))
const toolCall = tools[0] && rawToolCallObjOfAnthropicParams(tools[0])
const toolCallObj = toolCall ? { toolCall } : {}
onFinalMessage({ fullText, fullReasoning, anthropicReasoning, ...toolCallObj })
})
// on error

View file

@ -0,0 +1,80 @@
import { registerSingleton, InstantiationType } from '../../../../platform/instantiation/common/extensions.js'
import { promisify } from 'util'
import { exec as _exec } from 'child_process'
import { IVoidSCMService } from '../common/voidSCMTypes.js'
interface NumStat {
file: string
added: number
removed: number
}
const exec = promisify(_exec)
//8000 and 10 were chosen after some experimentation on small-to-moderately sized changes
const MAX_DIFF_LENGTH = 8000
const MAX_DIFF_FILES = 10
const git = async (command: string, path: string): Promise<string> => {
const { stdout, stderr } = await exec(`${command}`, { cwd: path })
if (stderr) {
throw new Error(stderr)
}
return stdout.trim()
}
const getNumStat = async (path: string, useStagedChanges: boolean): Promise<NumStat[]> => {
const staged = useStagedChanges ? '--staged' : ''
const output = await git(`git diff --numstat ${staged}`, path)
return output
.split('\n')
.map((line) => {
const [added, removed, file] = line.split('\t')
return {
file,
added: parseInt(added, 10) || 0,
removed: parseInt(removed, 10) || 0,
}
})
}
const getSampledDiff = async (file: string, path: string, useStagedChanges: boolean): Promise<string> => {
const staged = useStagedChanges ? '--staged' : ''
const diff = await git(`git diff --unified=0 --no-color ${staged} -- "${file}"`, path)
return diff.slice(0, MAX_DIFF_LENGTH)
}
const hasStagedChanges = async (path: string): Promise<boolean> => {
const output = await git('git diff --staged --name-only', path)
return output.length > 0
}
export class VoidSCMService implements IVoidSCMService {
readonly _serviceBrand: undefined
async gitStat(path: string): Promise<string> {
const useStagedChanges = await hasStagedChanges(path)
const staged = useStagedChanges ? '--staged' : ''
return git(`git diff --stat ${staged}`, path)
}
async gitSampledDiffs(path: string): Promise<string> {
const useStagedChanges = await hasStagedChanges(path)
const numStatList = await getNumStat(path, useStagedChanges)
const topFiles = numStatList
.sort((a, b) => (b.added + b.removed) - (a.added + a.removed))
.slice(0, MAX_DIFF_FILES)
const diffs = await Promise.all(topFiles.map(async ({ file }) => ({ file, diff: await getSampledDiff(file, path, useStagedChanges) })))
return diffs.map(({ file, diff }) => `==== ${file} ====\n${diff}`).join('\n\n')
}
gitBranch(path: string): Promise<string> {
return git('git branch --show-current', path)
}
gitLog(path: string): Promise<string> {
return git('git log --pretty=format:"%h|%s|%ad" --date=short --no-merges -n 5', path)
}
}
registerSingleton(IVoidSCMService, VoidSCMService, InstantiationType.Delayed)