Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions src/extension/prompt/common/repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 };
}
8 changes: 4 additions & 4 deletions src/extension/prompt/node/gitCommitMessageGenerator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -29,16 +29,16 @@ export class GitCommitMessageGenerator {
@IInteractionService private readonly interactionService: IInteractionService,
) { }

async generateGitCommitMessage(changes: Diff[], recentCommitMessages: RecentCommitMessages, attemptCount: number, token: CancellationToken): Promise<string | undefined> {
async generateGitCommitMessage(changes: Diff[], recentCommitMessages: RecentCommitMessages, repoContext: GitCommitRepoContext, attemptCount: number, token: CancellationToken): Promise<string | undefined> {
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(
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();
Expand Down
27 changes: 24 additions & 3 deletions src/extension/prompt/vscode-node/gitCommitMessageServiceImpl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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));
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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<GitCommitRepoContext> {
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));
Expand Down
21 changes: 17 additions & 4 deletions src/extension/prompts/node/git/gitCommitMessagePrompt.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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<GitCommitMessagePromptProps> {
Expand All @@ -40,6 +41,18 @@ export class GitCommitMessagePrompt extends PromptElement<GitCommitMessagePrompt
{this.props.recentCommitMessages.repository.map(message => `- ${message}\n`).join('')}
</Tag>
)}
{this.props.repoContext &&
<Tag priority={800} name='repoContext'>
<>
# REPOSITORY CONTEXT<br />
{this.props.repoContext.repositoryName ? <>Repository name: {this.props.repoContext.repositoryName}<br /></> : ''}
{this.props.repoContext.owner ? <>Owner: {this.props.repoContext.owner}<br /></> : ''}
{this.props.repoContext.headBranchName ? <>Current branch: {this.props.repoContext.headBranchName}<br /></> : ''}
{this.props.repoContext.defaultBranch ? <>Default branch: {this.props.repoContext.defaultBranch}<br /></> : ''}
{this.props.repoContext?.pullRequest ? <>Active pull request: {this.props.repoContext.pullRequest.title} ({this.props.repoContext.pullRequest.url})<br /></> : ''}
</>
</Tag>
}
<Tag priority={900} name='changes'>
{this.props.changes.map((change) => (
<>
Expand All @@ -61,7 +74,7 @@ export class GitCommitMessagePrompt extends PromptElement<GitCommitMessagePrompt
</Tag>
<Tag priority={900} name='reminder'>
Now generate a commit messages that describe the CODE CHANGES.<br />
DO NOT COPY commits from RECENT COMMITS, but it as reference for the commit style.<br />
DO NOT COPY commits from RECENT COMMITS, but use it as reference for the commit style.<br />
ONLY return a single markdown code block, NO OTHER PROSE!<br />
<UnsafeCodeBlock languageId='text' code='commit message goes here' />
</Tag>
Expand Down Expand Up @@ -89,9 +102,9 @@ class GitCommitMessageSystemRules extends PromptElement {
# First, think step-by-step:<br />
1. Analyze the CODE CHANGES thoroughly to understand what's been modified.<br />
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.<br />
3. Identify the purpose of the changes to answer the *why* for the commit messages, also considering the optionally provided RECENT USER COMMITS.<br />
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.<br />
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.<br />
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.<br />
7. Now only show your message, wrapped with a single markdown ```text codeblock! Do not provide any explanations or details<br />
</>
Expand Down
71 changes: 68 additions & 3 deletions test/prompts/gitCommitMessageGenerator.stest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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');
});

Expand Down Expand Up @@ -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');

Expand Down Expand Up @@ -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');
});
});