Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/copilot/src/extension/prompt/vscode-node/gitDiffService.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 { type CancellationToken, Uri, workspace } from 'vscode';
7
import { Diff, IGitDiffService } from '../../../platform/git/common/gitDiffService';
8
import { IGitExtensionService } from '../../../platform/git/common/gitExtensionService';
9
import { Change, Repository } from '../../../platform/git/vscode/git';
10
import { IIgnoreService } from '../../../platform/ignore/common/ignoreService';
11
import { ILogService } from '../../../platform/log/common/logService';
12
import { isUri } from '../../../util/common/types';
13
import { CancellationError } from '../../../util/vs/base/common/errors';
14
import * as path from '../../../util/vs/base/common/path';
15
import { isEqual } from '../../../util/vs/base/common/resources';
16
17
/**
18
* Maximum file size (in bytes) for reading untracked file content.
19
* Files larger than this will have their diff omitted.
20
*/
21
const MAX_UNTRACKED_FILE_SIZE = 1 * 1024 * 1024; // 1 MB
22
23
/**
24
* Maximum size (in characters) for a single diff output.
25
* Diffs larger than this will be truncated.
26
*/
27
const MAX_DIFF_SIZE = 100_000; // ~100KB
28
29
export class GitDiffService implements IGitDiffService {
30
declare readonly _serviceBrand: undefined;
31
32
constructor(
33
@IGitExtensionService private readonly _gitExtensionService: IGitExtensionService,
34
@IIgnoreService private readonly _ignoreService: IIgnoreService,
35
@ILogService private readonly _logService: ILogService
36
) { }
37
38
private async _resolveRepository(repositoryOrUri: Repository | Uri): Promise<Repository | null | undefined> {
39
if (isUri(repositoryOrUri)) {
40
const extensionApi = this._gitExtensionService.getExtensionApi();
41
return extensionApi?.getRepository(repositoryOrUri) ?? await extensionApi?.openRepository(repositoryOrUri) ?? extensionApi?.repositories.find((repo) => isEqual(repo.rootUri, repositoryOrUri));
42
}
43
return repositoryOrUri;
44
}
45
46
// Get the diff between the current state of the repository and the specified ref for each of the provided changes
47
async getWorkingTreeDiffsFromRef(repositoryOrUri: Repository | Uri, changes: Change[], ref: string, token?: CancellationToken): Promise<Diff[]> {
48
this._logService.debug(`[GitDiffService] Getting working tree diffs from ref ${ref} for ${changes.length} file(s)`);
49
50
const repository = await this._resolveRepository(repositoryOrUri);
51
if (!repository) {
52
this._logService.debug(`[GitDiffService] Repository not found for uri: ${repositoryOrUri.toString()}`);
53
return [];
54
}
55
56
const diffs: Diff[] = [];
57
for (const change of changes) {
58
if (token?.isCancellationRequested) {
59
throw new CancellationError();
60
}
61
62
if (await this._ignoreService.isCopilotIgnored(change.uri)) {
63
this._logService.debug(`[GitDiffService] Ignoring change due to content exclusion rule based on uri: ${change.uri.toString()}`);
64
continue;
65
}
66
67
let diff: string;
68
if (change.status === 7 /* UNTRACKED */) {
69
// For untracked files, generate a patch showing all content as additions
70
diff = await this._getUntrackedChangePatch(repository, change.uri);
71
} else {
72
// For all other changes, get diff from ref to current working tree state
73
diff = await repository.diffWith(ref, change.uri.fsPath);
74
}
75
76
diffs.push({
77
originalUri: change.originalUri,
78
renameUri: change.renameUri,
79
status: change.status,
80
uri: change.uri,
81
diff: this._truncateDiff(diff, change.uri)
82
});
83
}
84
85
this._logService.debug(`[GitDiffService] Working tree diffs from ref (after context exclusion): ${diffs.length} file(s)`);
86
87
return diffs;
88
}
89
90
async getChangeDiffs(repositoryOrUri: Repository | Uri, changes: Change[], token?: CancellationToken): Promise<Diff[]> {
91
this._logService.debug(`[GitDiffService] Changes (before context exclusion): ${changes.length} file(s)`);
92
93
const repository = await this._resolveRepository(repositoryOrUri);
94
if (!repository) {
95
this._logService.debug(`[GitDiffService] Repository not found for uri: ${repositoryOrUri.toString()}`);
96
return [];
97
}
98
99
const diffs: Diff[] = [];
100
for (const change of changes) {
101
if (token?.isCancellationRequested) {
102
throw new CancellationError();
103
}
104
105
if (await this._ignoreService.isCopilotIgnored(change.uri)) {
106
this._logService.debug(`[GitDiffService] Ignoring change due to content exclusion rule based on uri: ${change.uri.toString()}`);
107
continue;
108
}
109
110
let diff: string;
111
switch (change.status) {
112
case 0 /* INDEX_MODIFIED */:
113
case 1 /* INDEX_ADDED */:
114
case 2 /* INDEX_DELETED */:
115
case 3 /* INDEX_RENAMED */:
116
case 4 /* INDEX_COPIED */:
117
diff = await repository.diffIndexWithHEAD(change.uri.fsPath);
118
break;
119
case 7 /* UNTRACKED */:
120
diff = await this._getUntrackedChangePatch(repository, change.uri);
121
break;
122
default:
123
diff = await repository.diffWithHEAD(change.uri.fsPath);
124
break;
125
}
126
127
diffs.push({
128
originalUri: change.originalUri,
129
renameUri: change.renameUri,
130
status: change.status,
131
uri: change.uri,
132
diff: this._truncateDiff(diff, change.uri)
133
});
134
}
135
136
this._logService.debug(`[GitDiffService] Changes (after context exclusion): ${diffs.length} file(s)`);
137
138
return diffs;
139
}
140
141
private async _getUntrackedChangePatch(repository: Repository, resource: Uri): Promise<string> {
142
const patch: string[] = [];
143
const relativePath = path.relative(repository.rootUri.fsPath, resource.fsPath);
144
145
// Check file size before reading to avoid OOM with large/binary files
146
try {
147
const stat = await workspace.fs.stat(resource);
148
if (stat.size > MAX_UNTRACKED_FILE_SIZE) {
149
this._logService.debug(`[GitDiffService] Skipping untracked file (too large: ${stat.size} bytes): ${resource.toString()}`);
150
// Return a minimal patch header indicating the file is new but too large to diff
151
patch.push(`diff --git a/${relativePath} b/${relativePath}`);
152
patch.push('new file mode 100644');
153
patch.push('--- /dev/null', `+++ b/${relativePath}`);
154
patch.push(`\\ File too large to diff (${Math.round(stat.size / 1024)} KB)`);
155
return patch.join('\n') + '\n';
156
}
157
} catch {
158
// stat failed - proceed to try reading the file anyway
159
}
160
161
try {
162
const buffer = await workspace.fs.readFile(resource);
163
const content = buffer.toString();
164
165
// Header
166
patch.push(`diff --git a/${relativePath} b/${relativePath}`);
167
// 100644 is standard file mode for new git files. Saves us from trying to check file permissions and handling
168
// UNIX vs Windows permission differences. Skipping calculating the SHA1 hashes as well since they are not strictly necessary
169
// to apply the patch.
170
patch.push('new file mode 100644');
171
patch.push('--- /dev/null', `+++ b/${relativePath}`);
172
173
// For non-empty files, add range header and content (empty files omit this)
174
if (content.length > 0) {
175
const lines = content.split('\n');
176
if (content.endsWith('\n')) {
177
// Prevent an extra empty line at the end
178
lines.pop();
179
}
180
181
// Range header and content
182
patch.push(`@@ -0,0 +1,${lines.length} @@`);
183
patch.push(...lines.map(line => `+${line}`));
184
185
// Git standard to add this comment if the file does not end with a newline
186
if (!content.endsWith('\n')) {
187
patch.push('\\ No newline at end of file');
188
}
189
}
190
} catch (err) {
191
this._logService.warn(`[GitDiffService] Failed to generate patch file for untracked file: ${resource.toString()}: ${err}`);
192
}
193
194
// The patch itself should always end with a newline per git patch standards
195
return patch.join('\n') + '\n';
196
}
197
198
private _truncateDiff(diff: string, uri: Uri): string {
199
if (diff.length > MAX_DIFF_SIZE) {
200
this._logService.debug(`[GitDiffService] Truncating diff for ${uri.toString()} (${diff.length} chars -> ${MAX_DIFF_SIZE} chars)`);
201
return diff.substring(0, MAX_DIFF_SIZE) + '\n... [diff truncated]\n';
202
}
203
return diff;
204
}
205
}
206
207