Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/copilot/src/extension/prompt/vscode-node/gitCommitMessageServiceImpl.ts
13399 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import * as l10n from '@vscode/l10n';
7
import { ProgressLocation, Uri, window } from 'vscode';
8
import { compute4GramTextSimilarity } from '../../../platform/editSurvivalTracking/common/editSurvivalTracker';
9
import { IGitCommitMessageService } from '../../../platform/git/common/gitCommitMessageService';
10
import { IGitDiffService } from '../../../platform/git/common/gitDiffService';
11
import { IGitExtensionService } from '../../../platform/git/common/gitExtensionService';
12
import { API, Repository } from '../../../platform/git/vscode/git';
13
import { ITelemetryService } from '../../../platform/telemetry/common/telemetry';
14
import { CancellationToken } from '../../../util/vs/base/common/cancellation';
15
import { DisposableMap, DisposableStore } from '../../../util/vs/base/common/lifecycle';
16
import { basename } from '../../../util/vs/base/common/resources';
17
import { IInstantiationService } from '../../../util/vs/platform/instantiation/common/instantiation';
18
import { RecentCommitMessages } from '../common/repository';
19
import { GitCommitMessageGenerator } from '../node/gitCommitMessageGenerator';
20
21
interface CommitMessage {
22
readonly attemptCount: number;
23
readonly changes: string[];
24
readonly message: string;
25
}
26
27
export class GitCommitMessageServiceImpl implements IGitCommitMessageService {
28
29
declare readonly _serviceBrand: undefined;
30
31
private _gitExtensionApi: API | undefined;
32
private readonly _commitMessages = new Map<string, Map<string, CommitMessage>>();
33
34
private readonly _disposables = new DisposableStore();
35
private readonly _repositoryDisposables = new DisposableMap<Repository>();
36
37
constructor(
38
@IGitExtensionService private readonly _gitExtensionService: IGitExtensionService,
39
@IInstantiationService private readonly _instantiationService: IInstantiationService,
40
@ITelemetryService private readonly _telemetryService: ITelemetryService,
41
@IGitDiffService private readonly _gitDiffService: IGitDiffService,
42
) {
43
const initialize = () => {
44
this._disposables.add(this._gitExtensionApi!.onDidOpenRepository(this._onDidOpenRepository, this));
45
this._disposables.add(this._gitExtensionApi!.onDidCloseRepository(this._onDidCloseRepository, this));
46
47
for (const repository of this._gitExtensionApi!.repositories) {
48
this._onDidOpenRepository(repository);
49
}
50
};
51
52
this._gitExtensionApi = this._gitExtensionService.getExtensionApi();
53
54
if (this._gitExtensionApi) {
55
initialize();
56
} else {
57
this._disposables.add(this._gitExtensionService.onDidChange((status) => {
58
if (status.enabled) {
59
this._gitExtensionApi = this._gitExtensionService.getExtensionApi()!;
60
initialize();
61
}
62
}));
63
}
64
}
65
66
async generateCommitMessage(repository: Repository, cancellationToken: CancellationToken = CancellationToken.None): Promise<string | undefined> {
67
if (cancellationToken.isCancellationRequested) {
68
return undefined;
69
}
70
71
return window.withProgress({ location: ProgressLocation.SourceControl }, async () => {
72
try {
73
// Explicitly refresh (best effort) the repository state to make
74
// sure that the repository state is up-to-date before generating
75
// the commit message.
76
await repository.status();
77
} catch (err) { }
78
79
const indexChanges = repository.state.indexChanges.length;
80
const workingTreeChanges = repository.state.workingTreeChanges.length;
81
const untrackedChanges = repository.state.untrackedChanges?.length ?? 0;
82
83
if (indexChanges + workingTreeChanges + untrackedChanges === 0) {
84
window.showInformationMessage(l10n.t('Cannot generate a commit message because there are no changes.'));
85
return undefined;
86
}
87
88
const resources = repository.state.indexChanges.length > 0
89
// Index
90
? repository.state.indexChanges
91
// Working tree, untracked changes
92
: [
93
...repository.state.workingTreeChanges,
94
...repository.state.untrackedChanges ?? []
95
];
96
97
const changes = await this._gitDiffService.getChangeDiffs(repository, resources);
98
99
if (changes.length === 0) {
100
window.showInformationMessage(l10n.t('Cannot generate a commit message because the changes were excluded from the context due to content exclusion rules.'));
101
return undefined;
102
}
103
104
const diffs = changes.map(diff => diff.diff);
105
const attemptCount = this._getAttemptCount(repository, diffs);
106
const recentCommitMessages = await this._getRecentCommitMessages(repository);
107
108
const repositoryName = basename(repository.rootUri);
109
const branchName = repository.state.HEAD?.name ?? '';
110
const gitCommitMessageGenerator = this._instantiationService.createInstance(GitCommitMessageGenerator);
111
const commitMessage = await gitCommitMessageGenerator.generateGitCommitMessage(repositoryName, branchName, changes, recentCommitMessages, attemptCount, cancellationToken);
112
113
// Save generated commit message
114
if (commitMessage && repository.state.HEAD && repository.state.HEAD.commit) {
115
const commitMessages = this._commitMessages.get(repository.rootUri.toString()) ?? new Map<string, CommitMessage>();
116
commitMessages.set(repository.state.HEAD.commit, { attemptCount, changes: diffs, message: commitMessage });
117
118
this._commitMessages.set(repository.rootUri.toString(), commitMessages);
119
}
120
121
return commitMessage;
122
});
123
}
124
125
async getRepository(uri?: Uri): Promise<Repository | null> {
126
if (!this._gitExtensionApi) {
127
return null;
128
}
129
130
if (uri === undefined && this._gitExtensionApi.repositories.length === 1) {
131
return this._gitExtensionApi.repositories[0];
132
}
133
134
uri = uri ?? window.activeTextEditor?.document.uri;
135
if (!uri) {
136
return null;
137
}
138
139
const repository = await this._gitExtensionApi.openRepository(uri);
140
if (!repository) {
141
return null;
142
}
143
144
// Refresh repository state
145
await repository.status();
146
147
return repository;
148
}
149
150
private _getAttemptCount(repository: Repository, changes: string[]): number {
151
const commitMessages = this._commitMessages.get(repository.rootUri.toString());
152
const commitMessage = commitMessages?.get(repository.state.HEAD?.commit ?? '');
153
154
if (!commitMessage || commitMessage.changes.length !== changes.length) {
155
return 0;
156
}
157
158
for (let index = 0; index < changes.length; index++) {
159
if (commitMessage.changes[index] !== changes[index]) {
160
return 0;
161
}
162
}
163
164
return commitMessage.attemptCount + 1;
165
}
166
167
private async _getRecentCommitMessages(repository: Repository): Promise<RecentCommitMessages> {
168
const repositoryCommitMessages: string[] = [];
169
const userCommitMessages: string[] = [];
170
171
try {
172
// Last 5 commit messages (repository)
173
const commits = await repository.log({ maxEntries: 5 });
174
repositoryCommitMessages.push(...commits.map(commit => commit.message.split('\n')[0]));
175
176
// Last 5 commit messages (user)
177
const author =
178
await repository.getConfig('user.name') ??
179
await repository.getGlobalConfig('user.name');
180
181
const userCommits = await repository.log({ maxEntries: 5, author });
182
userCommitMessages.push(...userCommits.map(commit => commit.message.split('\n')[0]));
183
}
184
catch (err) { }
185
186
return { repository: repositoryCommitMessages, user: userCommitMessages };
187
}
188
189
private _onDidOpenRepository(repository: Repository): void {
190
if (typeof repository.onDidCommit !== undefined) {
191
this._repositoryDisposables.set(repository, repository.onDidCommit(() => this._onDidCommit(repository), this));
192
}
193
}
194
195
private _onDidCloseRepository(repository: Repository): void {
196
this._repositoryDisposables.deleteAndDispose(repository);
197
this._commitMessages.delete(repository.rootUri.toString());
198
}
199
200
private async _onDidCommit(repository: Repository): Promise<void> {
201
const HEAD = repository.state.HEAD;
202
if (!HEAD?.commit) {
203
return;
204
}
205
206
const commitMessages = this._commitMessages.get(repository.rootUri.toString());
207
if (!commitMessages) {
208
return;
209
}
210
211
// Commit details
212
const commit = await repository.getCommit(HEAD.commit);
213
const commitParent = commit.parents.length > 0 ? commit.parents[0] : '';
214
const commitMessage = commitMessages.get(commitParent);
215
216
if (!commitMessage) {
217
return;
218
}
219
220
// Compute survival rate
221
const survivalRateFourGram = compute4GramTextSimilarity(commit.message, commitMessage.message);
222
223
/* __GDPR__
224
"git.generateCommitMessageSurvival" : {
225
"owner": "lszomoru",
226
"comment": "Tracks how much of the generated git commit message has survived",
227
"attemptCount": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "comment": "How many times the user has retried." },
228
"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." }
229
}
230
*/
231
this._telemetryService.sendMSFTTelemetryEvent('git.generateCommitMessageSurvival', undefined, { attemptCount: commitMessage.attemptCount, survivalRateFourGram });
232
233
// Delete commit message
234
commitMessages.delete(commitParent);
235
this._commitMessages.set(repository.rootUri.toString(), commitMessages);
236
}
237
238
dispose(): void {
239
this._repositoryDisposables.dispose();
240
this._disposables.dispose();
241
}
242
}
243
244