Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/copilot/src/extension/chatSessions/vscode-node/claudeWorkspaceFolderServiceImpl.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 vscode from 'vscode';
7
import { IVSCodeExtensionContext } from '../../../platform/extContext/common/extensionContext';
8
import { IFileSystemService } from '../../../platform/filesystem/common/fileSystemService';
9
import { IGitService } from '../../../platform/git/common/gitService';
10
import { toGitUri } from '../../../platform/git/common/utils';
11
import { buildTempIndexEnv, getUncommittedFilePaths, parseGitChangesRaw } from '../../../platform/git/vscode-node/utils';
12
import { DiffChange } from '../../../platform/git/vscode/git';
13
import { ILogService } from '../../../platform/log/common/logService';
14
import * as path from '../../../util/vs/base/common/path';
15
import { Disposable } from '../../../util/vs/base/common/lifecycle';
16
import { generateUuid } from '../../../util/vs/base/common/uuid';
17
import { ChatSessionWorktreeFile } from '../common/chatSessionWorktreeService';
18
import { IClaudeWorkspaceFolderService } from '../common/claudeWorkspaceFolderService';
19
20
// #region Constants
21
22
const EMPTY_TREE_OBJECT = '4b825dc642cb6eb9a060e54bf8d69288fbee4904';
23
24
// #endregion
25
26
export class ClaudeWorkspaceFolderService extends Disposable implements IClaudeWorkspaceFolderService {
27
declare _serviceBrand: undefined;
28
29
private readonly _cache = new Map<string, vscode.ChatSessionChangedFile[]>();
30
private readonly _inflight = new Map<string, Promise<vscode.ChatSessionChangedFile[]>>();
31
32
constructor(
33
@IGitService private readonly _gitService: IGitService,
34
@ILogService private readonly _logService: ILogService,
35
@IVSCodeExtensionContext private readonly _extensionContext: IVSCodeExtensionContext,
36
@IFileSystemService private readonly _fileSystemService: IFileSystemService,
37
) {
38
super();
39
}
40
41
override dispose(): void {
42
this._cache.clear();
43
this._inflight.clear();
44
super.dispose();
45
}
46
47
async getWorkspaceChanges(
48
cwd: string,
49
gitBranch: string | undefined,
50
gitBaseBranch: string | undefined,
51
forceRefresh?: boolean,
52
): Promise<vscode.ChatSessionChangedFile[]> {
53
const cacheKey = `${cwd}\0${gitBranch ?? ''}\0${gitBaseBranch ?? ''}`;
54
55
if (!forceRefresh) {
56
const cached = this._cache.get(cacheKey);
57
if (cached) {
58
return cached;
59
}
60
}
61
62
const existing = this._inflight.get(cacheKey);
63
if (existing) {
64
return existing;
65
}
66
67
const promise = this._computeAndCacheChanges(cacheKey, cwd, gitBranch, gitBaseBranch);
68
this._inflight.set(cacheKey, promise);
69
try {
70
return await promise;
71
} finally {
72
this._inflight.delete(cacheKey);
73
}
74
}
75
76
private async _computeAndCacheChanges(
77
cacheKey: string,
78
cwd: string,
79
gitBranch: string | undefined,
80
gitBaseBranch: string | undefined,
81
): Promise<vscode.ChatSessionChangedFile[]> {
82
const result = await this.computeRepositoryChanges(cwd, gitBranch, gitBaseBranch);
83
if (!result) {
84
return [];
85
}
86
87
const originalRef = result.mergeBaseCommit ?? 'HEAD';
88
const changes = result.changes.map(change => new vscode.ChatSessionChangedFile(
89
vscode.Uri.file(change.filePath),
90
change.originalFilePath
91
? toGitUri(vscode.Uri.file(change.originalFilePath), originalRef)
92
: undefined,
93
change.modifiedFilePath
94
? vscode.Uri.file(change.modifiedFilePath)
95
: undefined,
96
change.statistics.additions,
97
change.statistics.deletions,
98
));
99
100
this._cache.set(cacheKey, changes);
101
return changes;
102
}
103
104
private async computeRepositoryChanges(
105
repositoryPath: string,
106
branchName: string | undefined,
107
baseBranchName: string | undefined,
108
): Promise<{
109
readonly changes: ChatSessionWorktreeFile[];
110
readonly mergeBaseCommit?: string;
111
} | undefined> {
112
const repository = await this._gitService.getRepository(vscode.Uri.file(repositoryPath));
113
if (!repository?.changes) {
114
this._logService.warn(`[ClaudeWorkspaceFolderService] No repository found at ${repositoryPath}`);
115
return undefined;
116
}
117
118
let resolvedBaseBranchName = baseBranchName;
119
if (!resolvedBaseBranchName && branchName && repository.headCommitHash) {
120
try {
121
const branchBase = await this._gitService.getBranchBase(repository.rootUri, branchName);
122
resolvedBaseBranchName = branchBase?.name;
123
} catch (error) {
124
this._logService.warn(`[ClaudeWorkspaceFolderService] Failed to resolve base branch for ${branchName}: ${error}`);
125
}
126
}
127
128
// Check for untracked changes, only if the session branch matches the current branch
129
const hasUntrackedChanges = branchName === repository.headBranchName
130
? [
131
...repository.changes?.workingTree ?? [],
132
...repository.changes?.untrackedChanges ?? [],
133
].some(change => change.status === 7 /* UNTRACKED */)
134
: false;
135
136
const diffChanges: DiffChange[] = [];
137
138
// If the repository is using a virtual file system, we need to
139
// disable rename detection to avoid expensive git operations
140
const noRenamesArg = repository.isUsingVirtualFileSystem
141
? ['--no-renames']
142
: [];
143
144
const mergeBaseArg = resolvedBaseBranchName
145
? ['--merge-base', resolvedBaseBranchName]
146
: ['HEAD'];
147
148
if (hasUntrackedChanges) {
149
// Tracked + untracked changes
150
const tmpDirName = `vscode-sessions-${generateUuid()}`;
151
const diffIndexFile = path.join(this._extensionContext.globalStorageUri.fsPath, tmpDirName, 'diff.index');
152
const pathspecFile = path.join(this._extensionContext.globalStorageUri.fsPath, tmpDirName, `pathspec.txt`);
153
154
const env = buildTempIndexEnv(repository, diffIndexFile);
155
156
try {
157
// Create temp index file directory
158
await this._fileSystemService.createDirectory(vscode.Uri.file(path.dirname(diffIndexFile)));
159
160
try {
161
// Populate temp index from HEAD, fall back to empty tree if no commits exist
162
await this._gitService.exec(repository.rootUri, ['read-tree', 'HEAD'], env);
163
} catch {
164
// Fall back to empty tree for repositories with no commits
165
await this._gitService.exec(repository.rootUri, ['read-tree', EMPTY_TREE_OBJECT], env);
166
}
167
168
// Stage entire working directory into temp index
169
const uncommittedFilePaths = getUncommittedFilePaths(repository);
170
await this._fileSystemService.writeFile(vscode.Uri.file(pathspecFile), new TextEncoder().encode(uncommittedFilePaths.join('\n')));
171
await this._gitService.exec(repository.rootUri, ['add', '-A', `--pathspec-from-file=${pathspecFile}`], env);
172
173
// Diff the temp index with the base branch
174
const result = await this._gitService.exec(repository.rootUri, ['diff', '--cached', '--raw', '--numstat', '--diff-filter=ADMR', ...noRenamesArg, '-z', ...mergeBaseArg, '--'], env);
175
diffChanges.push(...parseGitChangesRaw(repository.rootUri.fsPath, result));
176
} catch (error) {
177
this._logService.error(`[ClaudeWorkspaceFolderService] Error while processing workspace changes: ${error}`);
178
return undefined;
179
} finally {
180
try {
181
await this._fileSystemService.delete(vscode.Uri.file(path.dirname(diffIndexFile)), { recursive: true });
182
} catch (error) {
183
this._logService.error(`[ClaudeWorkspaceFolderService] Error while cleaning up temp index file: ${error}`);
184
}
185
}
186
} else {
187
// Tracked changes
188
try {
189
const result = await this._gitService.exec(repository.rootUri, ['diff', '--raw', '--numstat', '--diff-filter=ADMR', ...noRenamesArg, '-z', ...mergeBaseArg, '--']);
190
diffChanges.push(...parseGitChangesRaw(repository.rootUri.fsPath, result));
191
} catch (error) {
192
this._logService.error(`[ClaudeWorkspaceFolderService] Error while processing workspace changes: ${error}`);
193
return undefined;
194
}
195
}
196
197
// Since the diff may be computed using the merge base commit of the current
198
// branch and the base branch, we need to compute it as well so that we can use
199
// it as the originalRef (left-hand side) of the diff editor
200
let mergeBaseCommit: string | undefined;
201
try {
202
if (branchName && resolvedBaseBranchName) {
203
mergeBaseCommit = await this._gitService.getMergeBase(repository.rootUri, branchName, resolvedBaseBranchName);
204
}
205
} catch (error) {
206
this._logService.error(`[ClaudeWorkspaceFolderService] Error while getting merge base (${branchName}, ${resolvedBaseBranchName}): ${error}`);
207
}
208
209
const changes = diffChanges.map(change => ({
210
filePath: change.uri.fsPath,
211
originalFilePath: change.status !== 1 /* INDEX_ADDED */
212
? change.originalUri?.fsPath
213
: undefined,
214
modifiedFilePath: change.status !== 6 /* DELETED */
215
? change.uri.fsPath
216
: undefined,
217
statistics: {
218
additions: change.insertions,
219
deletions: change.deletions
220
}
221
} satisfies ChatSessionWorktreeFile));
222
223
return { changes, mergeBaseCommit };
224
}
225
}
226
227