Path: blob/main/extensions/copilot/src/extension/prompt/vscode-node/gitCommitMessageServiceImpl.ts
13399 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import * as l10n from '@vscode/l10n';6import { ProgressLocation, Uri, window } from 'vscode';7import { compute4GramTextSimilarity } from '../../../platform/editSurvivalTracking/common/editSurvivalTracker';8import { IGitCommitMessageService } from '../../../platform/git/common/gitCommitMessageService';9import { IGitDiffService } from '../../../platform/git/common/gitDiffService';10import { IGitExtensionService } from '../../../platform/git/common/gitExtensionService';11import { API, Repository } from '../../../platform/git/vscode/git';12import { ITelemetryService } from '../../../platform/telemetry/common/telemetry';13import { CancellationToken } from '../../../util/vs/base/common/cancellation';14import { DisposableMap, DisposableStore } from '../../../util/vs/base/common/lifecycle';15import { basename } from '../../../util/vs/base/common/resources';16import { IInstantiationService } from '../../../util/vs/platform/instantiation/common/instantiation';17import { RecentCommitMessages } from '../common/repository';18import { GitCommitMessageGenerator } from '../node/gitCommitMessageGenerator';1920interface CommitMessage {21readonly attemptCount: number;22readonly changes: string[];23readonly message: string;24}2526export class GitCommitMessageServiceImpl implements IGitCommitMessageService {2728declare readonly _serviceBrand: undefined;2930private _gitExtensionApi: API | undefined;31private readonly _commitMessages = new Map<string, Map<string, CommitMessage>>();3233private readonly _disposables = new DisposableStore();34private readonly _repositoryDisposables = new DisposableMap<Repository>();3536constructor(37@IGitExtensionService private readonly _gitExtensionService: IGitExtensionService,38@IInstantiationService private readonly _instantiationService: IInstantiationService,39@ITelemetryService private readonly _telemetryService: ITelemetryService,40@IGitDiffService private readonly _gitDiffService: IGitDiffService,41) {42const initialize = () => {43this._disposables.add(this._gitExtensionApi!.onDidOpenRepository(this._onDidOpenRepository, this));44this._disposables.add(this._gitExtensionApi!.onDidCloseRepository(this._onDidCloseRepository, this));4546for (const repository of this._gitExtensionApi!.repositories) {47this._onDidOpenRepository(repository);48}49};5051this._gitExtensionApi = this._gitExtensionService.getExtensionApi();5253if (this._gitExtensionApi) {54initialize();55} else {56this._disposables.add(this._gitExtensionService.onDidChange((status) => {57if (status.enabled) {58this._gitExtensionApi = this._gitExtensionService.getExtensionApi()!;59initialize();60}61}));62}63}6465async generateCommitMessage(repository: Repository, cancellationToken: CancellationToken = CancellationToken.None): Promise<string | undefined> {66if (cancellationToken.isCancellationRequested) {67return undefined;68}6970return window.withProgress({ location: ProgressLocation.SourceControl }, async () => {71try {72// Explicitly refresh (best effort) the repository state to make73// sure that the repository state is up-to-date before generating74// the commit message.75await repository.status();76} catch (err) { }7778const indexChanges = repository.state.indexChanges.length;79const workingTreeChanges = repository.state.workingTreeChanges.length;80const untrackedChanges = repository.state.untrackedChanges?.length ?? 0;8182if (indexChanges + workingTreeChanges + untrackedChanges === 0) {83window.showInformationMessage(l10n.t('Cannot generate a commit message because there are no changes.'));84return undefined;85}8687const resources = repository.state.indexChanges.length > 088// Index89? repository.state.indexChanges90// Working tree, untracked changes91: [92...repository.state.workingTreeChanges,93...repository.state.untrackedChanges ?? []94];9596const changes = await this._gitDiffService.getChangeDiffs(repository, resources);9798if (changes.length === 0) {99window.showInformationMessage(l10n.t('Cannot generate a commit message because the changes were excluded from the context due to content exclusion rules.'));100return undefined;101}102103const diffs = changes.map(diff => diff.diff);104const attemptCount = this._getAttemptCount(repository, diffs);105const recentCommitMessages = await this._getRecentCommitMessages(repository);106107const repositoryName = basename(repository.rootUri);108const branchName = repository.state.HEAD?.name ?? '';109const gitCommitMessageGenerator = this._instantiationService.createInstance(GitCommitMessageGenerator);110const commitMessage = await gitCommitMessageGenerator.generateGitCommitMessage(repositoryName, branchName, changes, recentCommitMessages, attemptCount, cancellationToken);111112// Save generated commit message113if (commitMessage && repository.state.HEAD && repository.state.HEAD.commit) {114const commitMessages = this._commitMessages.get(repository.rootUri.toString()) ?? new Map<string, CommitMessage>();115commitMessages.set(repository.state.HEAD.commit, { attemptCount, changes: diffs, message: commitMessage });116117this._commitMessages.set(repository.rootUri.toString(), commitMessages);118}119120return commitMessage;121});122}123124async getRepository(uri?: Uri): Promise<Repository | null> {125if (!this._gitExtensionApi) {126return null;127}128129if (uri === undefined && this._gitExtensionApi.repositories.length === 1) {130return this._gitExtensionApi.repositories[0];131}132133uri = uri ?? window.activeTextEditor?.document.uri;134if (!uri) {135return null;136}137138const repository = await this._gitExtensionApi.openRepository(uri);139if (!repository) {140return null;141}142143// Refresh repository state144await repository.status();145146return repository;147}148149private _getAttemptCount(repository: Repository, changes: string[]): number {150const commitMessages = this._commitMessages.get(repository.rootUri.toString());151const commitMessage = commitMessages?.get(repository.state.HEAD?.commit ?? '');152153if (!commitMessage || commitMessage.changes.length !== changes.length) {154return 0;155}156157for (let index = 0; index < changes.length; index++) {158if (commitMessage.changes[index] !== changes[index]) {159return 0;160}161}162163return commitMessage.attemptCount + 1;164}165166private async _getRecentCommitMessages(repository: Repository): Promise<RecentCommitMessages> {167const repositoryCommitMessages: string[] = [];168const userCommitMessages: string[] = [];169170try {171// Last 5 commit messages (repository)172const commits = await repository.log({ maxEntries: 5 });173repositoryCommitMessages.push(...commits.map(commit => commit.message.split('\n')[0]));174175// Last 5 commit messages (user)176const author =177await repository.getConfig('user.name') ??178await repository.getGlobalConfig('user.name');179180const userCommits = await repository.log({ maxEntries: 5, author });181userCommitMessages.push(...userCommits.map(commit => commit.message.split('\n')[0]));182}183catch (err) { }184185return { repository: repositoryCommitMessages, user: userCommitMessages };186}187188private _onDidOpenRepository(repository: Repository): void {189if (typeof repository.onDidCommit !== undefined) {190this._repositoryDisposables.set(repository, repository.onDidCommit(() => this._onDidCommit(repository), this));191}192}193194private _onDidCloseRepository(repository: Repository): void {195this._repositoryDisposables.deleteAndDispose(repository);196this._commitMessages.delete(repository.rootUri.toString());197}198199private async _onDidCommit(repository: Repository): Promise<void> {200const HEAD = repository.state.HEAD;201if (!HEAD?.commit) {202return;203}204205const commitMessages = this._commitMessages.get(repository.rootUri.toString());206if (!commitMessages) {207return;208}209210// Commit details211const commit = await repository.getCommit(HEAD.commit);212const commitParent = commit.parents.length > 0 ? commit.parents[0] : '';213const commitMessage = commitMessages.get(commitParent);214215if (!commitMessage) {216return;217}218219// Compute survival rate220const survivalRateFourGram = compute4GramTextSimilarity(commit.message, commitMessage.message);221222/* __GDPR__223"git.generateCommitMessageSurvival" : {224"owner": "lszomoru",225"comment": "Tracks how much of the generated git commit message has survived",226"attemptCount": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "comment": "How many times the user has retried." },227"survivalRateFourGram": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "comment": "The rate between 0 and 1 of how much of the suggested git commit message was used when the code change was committed." }228}229*/230this._telemetryService.sendMSFTTelemetryEvent('git.generateCommitMessageSurvival', undefined, { attemptCount: commitMessage.attemptCount, survivalRateFourGram });231232// Delete commit message233commitMessages.delete(commitParent);234this._commitMessages.set(repository.rootUri.toString(), commitMessages);235}236237dispose(): void {238this._repositoryDisposables.dispose();239this._disposables.dispose();240}241}242243244