From e888269bc88a29d54561c0ce75e4e4b4389db377 Mon Sep 17 00:00:00 2001 From: Stephan Steinfurt Date: Sun, 17 Aug 2025 00:42:39 +0200 Subject: [PATCH 1/2] Fix typos in git commit message prompt --- src/extension/prompt/node/gitCommitMessageGenerator.ts | 2 +- src/extension/prompts/node/git/gitCommitMessagePrompt.tsx | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/extension/prompt/node/gitCommitMessageGenerator.ts b/src/extension/prompt/node/gitCommitMessageGenerator.ts index ab0cd15d8..ddaa588b9 100644 --- a/src/extension/prompt/node/gitCommitMessageGenerator.ts +++ b/src/extension/prompt/node/gitCommitMessageGenerator.ts @@ -38,7 +38,7 @@ export class GitCommitMessageGenerator { const temperature = Math.min( this.conversationOptions.temperature * (1 + attemptCount), - 2 /* MAX temperature - https://platform.openai.com/docs/api-reference/chat/create#chat/create-temperature */ + 2 /* MAX temperature - https://platform.openai.com/docs/api-reference/chat/create#chat_create-temperature */ ); const requestStartTime = Date.now(); diff --git a/src/extension/prompts/node/git/gitCommitMessagePrompt.tsx b/src/extension/prompts/node/git/gitCommitMessagePrompt.tsx index 7c817c356..09863874b 100644 --- a/src/extension/prompts/node/git/gitCommitMessagePrompt.tsx +++ b/src/extension/prompts/node/git/gitCommitMessagePrompt.tsx @@ -61,7 +61,7 @@ export class GitCommitMessagePrompt extends PromptElement Now generate a commit messages that describe the CODE CHANGES.
- DO NOT COPY commits from RECENT COMMITS, but it as reference for the commit style.
+ DO NOT COPY commits from RECENT COMMITS, but use it as reference for the commit style.
ONLY return a single markdown code block, NO OTHER PROSE!
@@ -91,7 +91,7 @@ class GitCommitMessageSystemRules extends PromptElement { 2. Use the ORIGINAL CODE to understand the context of the CODE CHANGES. Use the line numbers to map the CODE CHANGES to the ORIGINAL CODE.
3. Identify the purpose of the changes to answer the *why* for the commit messages, also considering the optionally provided RECENT USER COMMITS.
4. Review the provided RECENT REPOSITORY COMMITS to identify established commit message conventions. Focus on the format and style, ignoring commit-specific details like refs, tags, and authors.
- 5. Generate a thoughtful and succinct commit message for the given CODE CHANGES. It MUST follow the the established writing conventions. + 5. Generate a thoughtful and succinct commit message for the given CODE CHANGES. It MUST follow the established writing conventions. 6. Remove any meta information like issue references, tags, or author names from the commit message. The developer will add them.
7. Now only show your message, wrapped with a single markdown ```text codeblock! Do not provide any explanations or details
From 028113053934129b79729a1b557e71aae8c1c0d0 Mon Sep 17 00:00:00 2001 From: Stephan Steinfurt Date: Sun, 17 Aug 2025 01:10:25 +0200 Subject: [PATCH 2/2] Enhance git commit prompt with repository context The most important information is the branch name which can be helpful to find out what the implemented feature is about or to extract ticket numbers to be referenced in the commit message. GitHub information about the repository owner/name and possible pull request information is optional. The implementation is based on [1]. [1] https://github.com/microsoft/vscode-copilot-chat/blob/aa45bf559489cc5d21e93bd5cce7388056819222/src/extension/prompts/node/agent/agentPrompt.tsx#L497-L524 --- src/extension/prompt/common/repository.ts | 8 +++ .../prompt/node/gitCommitMessageGenerator.ts | 6 +- .../gitCommitMessageServiceImpl.ts | 27 ++++++- .../node/git/gitCommitMessagePrompt.tsx | 17 ++++- .../gitCommitMessageGenerator.stest.ts | 71 ++++++++++++++++++- 5 files changed, 118 insertions(+), 11 deletions(-) diff --git a/src/extension/prompt/common/repository.ts b/src/extension/prompt/common/repository.ts index 32ef5d8d3..7fccb670f 100644 --- a/src/extension/prompt/common/repository.ts +++ b/src/extension/prompt/common/repository.ts @@ -7,3 +7,11 @@ export interface RecentCommitMessages { readonly repository: string[]; readonly user: string[]; } + +export interface GitCommitRepoContext { + readonly repositoryName?: string; + readonly owner?: string; + readonly headBranchName?: string; + readonly defaultBranch?: string; + readonly pullRequest?: { title: string; url: string }; +} diff --git a/src/extension/prompt/node/gitCommitMessageGenerator.ts b/src/extension/prompt/node/gitCommitMessageGenerator.ts index ddaa588b9..4fb145472 100644 --- a/src/extension/prompt/node/gitCommitMessageGenerator.ts +++ b/src/extension/prompt/node/gitCommitMessageGenerator.ts @@ -14,7 +14,7 @@ import { ITelemetryService } from '../../../platform/telemetry/common/telemetry' import { IInstantiationService } from '../../../util/vs/platform/instantiation/common/instantiation'; import { PromptRenderer } from '../../prompts/node/base/promptRenderer'; import { GitCommitMessagePrompt } from '../../prompts/node/git/gitCommitMessagePrompt'; -import { RecentCommitMessages } from '../common/repository'; +import { GitCommitRepoContext, RecentCommitMessages } from '../common/repository'; type ResponseFormat = 'noTextCodeBlock' | 'oneTextCodeBlock' | 'multipleTextCodeBlocks'; @@ -29,11 +29,11 @@ export class GitCommitMessageGenerator { @IInteractionService private readonly interactionService: IInteractionService, ) { } - async generateGitCommitMessage(changes: Diff[], recentCommitMessages: RecentCommitMessages, attemptCount: number, token: CancellationToken): Promise { + async generateGitCommitMessage(changes: Diff[], recentCommitMessages: RecentCommitMessages, repoContext: GitCommitRepoContext, attemptCount: number, token: CancellationToken): Promise { const startTime = Date.now(); const endpoint = await this.endpointProvider.getChatEndpoint('gpt-4o-mini'); - const promptRenderer = PromptRenderer.create(this.instantiationService, endpoint, GitCommitMessagePrompt, { changes, recentCommitMessages }); + const promptRenderer = PromptRenderer.create(this.instantiationService, endpoint, GitCommitMessagePrompt, { changes, recentCommitMessages, repoContext }); const prompt = await promptRenderer.render(undefined, undefined); const temperature = Math.min( diff --git a/src/extension/prompt/vscode-node/gitCommitMessageServiceImpl.ts b/src/extension/prompt/vscode-node/gitCommitMessageServiceImpl.ts index 4ba367d4d..1d7b7fe87 100644 --- a/src/extension/prompt/vscode-node/gitCommitMessageServiceImpl.ts +++ b/src/extension/prompt/vscode-node/gitCommitMessageServiceImpl.ts @@ -3,17 +3,19 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { ProgressLocation, Uri, l10n, window } from 'vscode'; +import { l10n, ProgressLocation, Uri, window } from 'vscode'; import { compute4GramTextSimilarity } from '../../../platform/editSurvivalTracking/common/editSurvivalTracker'; import { IGitCommitMessageService } from '../../../platform/git/common/gitCommitMessageService'; import { IGitDiffService } from '../../../platform/git/common/gitDiffService'; import { IGitExtensionService } from '../../../platform/git/common/gitExtensionService'; +import { getGitHubRepoInfoFromContext, IGitService, RepoContext } from '../../../platform/git/common/gitService'; import { API, Repository } from '../../../platform/git/vscode/git'; import { ITelemetryService } from '../../../platform/telemetry/common/telemetry'; import { CancellationToken } from '../../../util/vs/base/common/cancellation'; import { DisposableMap, DisposableStore } from '../../../util/vs/base/common/lifecycle'; import { IInstantiationService } from '../../../util/vs/platform/instantiation/common/instantiation'; -import { RecentCommitMessages } from '../common/repository'; +import { GitHubPullRequestProviders } from '../../conversation/node/githubPullRequestProviders'; +import { GitCommitRepoContext, RecentCommitMessages } from '../common/repository'; import { GitCommitMessageGenerator } from '../node/gitCommitMessageGenerator'; interface CommitMessage { @@ -37,6 +39,7 @@ export class GitCommitMessageServiceImpl implements IGitCommitMessageService { @IInstantiationService private readonly _instantiationService: IInstantiationService, @ITelemetryService private readonly _telemetryService: ITelemetryService, @IGitDiffService private readonly _gitDiffService: IGitDiffService, + @IGitService private readonly _gitService: IGitService, ) { const initialize = () => { this._disposables.add(this._gitExtensionApi!.onDidOpenRepository(this._onDidOpenRepository, this)); @@ -96,8 +99,12 @@ export class GitCommitMessageServiceImpl implements IGitCommitMessageService { const attemptCount = this._getAttemptCount(repository, diffs); const recentCommitMessages = await this._getRecentCommitMessages(repository); + const repoContext = this._gitService.activeRepository?.get(); + const prProvider = this._instantiationService.createInstance(GitHubPullRequestProviders); + const gitCommitRepoContext = await this._getGitCommitRepoContext(repository, repoContext, prProvider); + const gitCommitMessageGenerator = this._instantiationService.createInstance(GitCommitMessageGenerator); - const commitMessage = await gitCommitMessageGenerator.generateGitCommitMessage(changes, recentCommitMessages, attemptCount, cancellationToken); + const commitMessage = await gitCommitMessageGenerator.generateGitCommitMessage(changes, recentCommitMessages, gitCommitRepoContext, attemptCount, cancellationToken); // Save generated commit message if (commitMessage && repository.state.HEAD && repository.state.HEAD.commit) { @@ -163,6 +170,20 @@ export class GitCommitMessageServiceImpl implements IGitCommitMessageService { return { repository: repositoryCommitMessages, user: userCommitMessages }; } + private async _getGitCommitRepoContext(repository: Repository, repoContext: RepoContext | undefined, prProvider: GitHubPullRequestProviders): Promise { + const headBranchName = repository.state.HEAD?.name; + const gitHubRepoInfo = repoContext && getGitHubRepoInfoFromContext(repoContext); + const gitHubRepoDescription = await prProvider.getRepositoryDescription(repository.rootUri); + + return { + headBranchName, + repositoryName: gitHubRepoInfo?.id.repo, + owner: gitHubRepoInfo?.id.org, + defaultBranch: gitHubRepoDescription?.defaultBranch, + pullRequest: gitHubRepoDescription?.pullRequest + }; + } + private _onDidOpenRepository(repository: Repository): void { if (typeof repository.onDidCommit !== undefined) { this._repositoryDisposables.set(repository, repository.onDidCommit(() => this._onDidCommit(repository), this)); diff --git a/src/extension/prompts/node/git/gitCommitMessagePrompt.tsx b/src/extension/prompts/node/git/gitCommitMessagePrompt.tsx index 09863874b..47a553466 100644 --- a/src/extension/prompts/node/git/gitCommitMessagePrompt.tsx +++ b/src/extension/prompts/node/git/gitCommitMessagePrompt.tsx @@ -5,7 +5,7 @@ import { BasePromptElementProps, PromptElement, SystemMessage, UserMessage } from '@vscode/prompt-tsx'; import { Diff } from '../../../../platform/git/common/gitDiffService'; import { basename } from '../../../../util/vs/base/common/path'; -import { RecentCommitMessages } from '../../../prompt/common/repository'; +import { GitCommitRepoContext, RecentCommitMessages } from '../../../prompt/common/repository'; import { ResponseTranslationRules } from '../base/responseTranslationRules'; import { SafetyRules } from '../base/safetyRules'; import { Tag } from '../base/tag'; @@ -16,6 +16,7 @@ import { UnsafeCodeBlock } from '../panel/unsafeElements'; export interface GitCommitMessagePromptProps extends BasePromptElementProps { readonly changes: Diff[]; readonly recentCommitMessages: RecentCommitMessages; + readonly repoContext: GitCommitRepoContext; } export class GitCommitMessagePrompt extends PromptElement { @@ -40,6 +41,18 @@ export class GitCommitMessagePrompt extends PromptElement `- ${message}\n`).join('')} )} + {this.props.repoContext && + + <> + # REPOSITORY CONTEXT
+ {this.props.repoContext.repositoryName ? <>Repository name: {this.props.repoContext.repositoryName}
: ''} + {this.props.repoContext.owner ? <>Owner: {this.props.repoContext.owner}
: ''} + {this.props.repoContext.headBranchName ? <>Current branch: {this.props.repoContext.headBranchName}
: ''} + {this.props.repoContext.defaultBranch ? <>Default branch: {this.props.repoContext.defaultBranch}
: ''} + {this.props.repoContext?.pullRequest ? <>Active pull request: {this.props.repoContext.pullRequest.title} ({this.props.repoContext.pullRequest.url})
: ''} + +
+ } {this.props.changes.map((change) => ( <> @@ -89,7 +102,7 @@ class GitCommitMessageSystemRules extends PromptElement { # First, think step-by-step:
1. Analyze the CODE CHANGES thoroughly to understand what's been modified.
2. Use the ORIGINAL CODE to understand the context of the CODE CHANGES. Use the line numbers to map the CODE CHANGES to the ORIGINAL CODE.
- 3. Identify the purpose of the changes to answer the *why* for the commit messages, also considering the optionally provided RECENT USER COMMITS.
+ 3. Identify the purpose of the changes to answer the *why* for the commit messages, also considering the REPOSITORY CONTEXT and the optionally provided RECENT USER COMMITS.
4. Review the provided RECENT REPOSITORY COMMITS to identify established commit message conventions. Focus on the format and style, ignoring commit-specific details like refs, tags, and authors.
5. Generate a thoughtful and succinct commit message for the given CODE CHANGES. It MUST follow the established writing conventions. 6. Remove any meta information like issue references, tags, or author names from the commit message. The developer will add them.
diff --git a/test/prompts/gitCommitMessageGenerator.stest.ts b/test/prompts/gitCommitMessageGenerator.stest.ts index a10b408c0..dc8a1bd45 100644 --- a/test/prompts/gitCommitMessageGenerator.stest.ts +++ b/test/prompts/gitCommitMessageGenerator.stest.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import assert from 'assert'; import { GitCommitMessageGenerator } from '../../src/extension/prompt/node/gitCommitMessageGenerator'; +import { ConfigKey } from '../../src/platform/configuration/common/configurationService'; import { Diff } from '../../src/platform/git/common/gitDiffService'; import { TestWorkspaceService } from '../../src/platform/test/node/testWorkspaceService'; import { IWorkspaceService } from '../../src/platform/workspace/common/workspaceService'; @@ -46,7 +47,7 @@ index 0877b83..6260896 100644 ]; const generator = instantiationService.createInstance(GitCommitMessageGenerator); - const message = await generator.generateGitCommitMessage(changes, { repository: [], user: [] }, 0, CancellationToken.None); + const message = await generator.generateGitCommitMessage(changes, { repository: [], user: [] }, {}, 0, CancellationToken.None); assert.ok(message !== undefined, 'Failed to generate a commit message'); }); @@ -90,13 +91,16 @@ index 0877b83..6260896 100644 } satisfies Diff ]; + const branchName: string = "abc-perform-refactorings"; + const generator = instantiationService.createInstance(GitCommitMessageGenerator); - const message = await generator.generateGitCommitMessage(changes, { repository: repoCommits, user: userCommits }, 0, CancellationToken.None); + const message = await generator.generateGitCommitMessage(changes, { repository: repoCommits, user: userCommits }, { headBranchName: branchName }, 0, CancellationToken.None); assert.ok(message !== undefined, 'Failed to generate a commit message'); assert.ok(!userCommits.some(commit => message.toLowerCase().includes(commit)), 'Commit message contains a user commit'); assert.ok(!repoCommits.some(commit => message.toLowerCase().includes(commit)), 'Commit message contains a repo commit'); assert.ok(['fix:', 'chore:', 'feat:', 'refactor:'].some(prefix => message.toLowerCase().startsWith(prefix)), 'Commit message does not follow the conventional commits format'); + assert.ok(!message.includes(branchName), 'Commit message does not contain branch name'); assert.ok(!message.includes('example.com'), 'Commit message contains the email address'); assert.ok(!/#\d+/.test(message), 'Commit message does include an issue reference'); @@ -143,11 +147,72 @@ index 0877b83..6260896 100644 ]; const generator = instantiationService.createInstance(GitCommitMessageGenerator); - const message = await generator.generateGitCommitMessage(changes, { repository: repoCommits, user: userCommits }, 0, CancellationToken.None); + const message = await generator.generateGitCommitMessage(changes, { repository: repoCommits, user: userCommits }, {}, 0, CancellationToken.None); assert.ok(message !== undefined, 'Failed to generate a commit message'); assert.ok(!userCommits.some(commit => message.toLowerCase().includes(commit)), 'Commit message contains a user commit'); assert.ok(!repoCommits.some(commit => message.toLowerCase().includes(commit)), 'Commit message contains a repo commit'); assert.ok(!['fix:', 'feat:', 'chore:', 'docs:', 'style:', 'refactor:'].some(prefix => message.toLowerCase().startsWith(prefix)), 'Commit message should not use conventional commits format'); }); + + + const commitMessageConfig = [ + { + key: ConfigKey.CommitMessageGenerationInstructions, + value: [ + { "text": "In this repository, we use conventional commits. The branch name usually encodes the feature currently being worked on. Use the feature name as the conventional commit scope. If the branch is called 'XYZ-1000-setup-project' the commit should start with 'feat(setup):' where the scope 'setup' references the current feature. Keep the scope to one word only." }, + { "text": "Every commit must reference a ticket number in the very last line with one empty line before. Extract the ticket number from the branch name. Usually, it will be 'XYZ-' followed by a number. If the branch is called 'XYZ-1000-setup-project' the ticket number is 'XYZ-1000'." } + ] + } + ]; + + stest({ description: 'Uses repository context along with custom instructions', configurations: commitMessageConfig, language: 'python' }, async (testingServiceCollection) => { + const content = ` +def show_exomple(): + print("This is an example.")`; + + const document = ExtHostDocumentData.create(URI.file('main.py'), content, 'python').document; + testingServiceCollection.define(IWorkspaceService, new TestWorkspaceService(undefined, [document])); + + const accessor = testingServiceCollection.createTestingAccessor(); + const instantiationService = accessor.get(IInstantiationService); + + const diff = `diff --git a/sample.py b/sample.py +index 0877b83..6260896 100644 +--- a/sample.py ++++ b/sample.py +@@ -1,3 +1,3 @@ +-def show_exomple(): ++def show_example(): + print("This is an example.") +\ No newline at end of file`; + + const repoCommits = [ + 'feat(setup): Initial project setup\n\nXYZ-1000', + 'feat(setup): Install dependencies\n\nXYZ-1000' + ]; + + const userCommits = [ + 'Add sample' + ]; + + const changes: Diff[] = [ + { + uri: document.uri, + originalUri: document.uri, + renameUri: undefined, + status: 5 /* Modified */, + diff + } satisfies Diff + ]; + + const headBranchName = "XYZ-1234-implement-login"; + + const generator = instantiationService.createInstance(GitCommitMessageGenerator); + const message = await generator.generateGitCommitMessage(changes, { repository: repoCommits, user: userCommits }, { headBranchName }, 0, CancellationToken.None); + + assert.ok(message !== undefined, 'Failed to generate a commit message'); + assert.ok(message.startsWith('feat(login):'), 'Failed to extract feature from branch name'); + assert.ok(message.includes('XYZ-1234'), 'Failed to extract ticket number from branch name'); + }); });