Path: blob/main/extensions/copilot/src/extension/chatSessions/vscode-node/chatSessionWorktreeCheckpointServiceImpl.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 { promises as fs } from 'fs';67import { Uri } from 'vscode';8import { IVSCodeExtensionContext } from '../../../platform/extContext/common/extensionContext';9import { IGitService, RepoContext } from '../../../platform/git/common/gitService';10import { ILogService } from '../../../platform/log/common/logService';11import { Disposable } from '../../../util/vs/base/common/lifecycle';12import * as path from '../../../util/vs/base/common/path';13import { generateUuid } from '../../../util/vs/base/common/uuid';14import { IAgentSessionsWorkspace } from '../common/agentSessionsWorkspace';15import { IChatSessionMetadataStore } from '../common/chatSessionMetadataStore';16import { IChatSessionWorkspaceFolderService } from '../common/chatSessionWorkspaceFolderService';17import { IChatSessionWorktreeCheckpointService } from '../common/chatSessionWorktreeCheckpointService';18import { IChatSessionWorktreeService } from '../common/chatSessionWorktreeService';19import { buildTempIndexEnv, getUncommittedFilePaths } from '../../../platform/git/vscode-node/utils';2021const CHECKPOINT_REF_PREFIX = 'refs/sessions/';2223function getCheckpointRef(sessionId: string, turnNumber: number): string {24return `${CHECKPOINT_REF_PREFIX}${sessionId}/checkpoints/turn/${turnNumber}`;25}2627export class ChatSessionWorktreeCheckpointService extends Disposable implements IChatSessionWorktreeCheckpointService {28declare _serviceBrand: undefined;2930constructor(31@IAgentSessionsWorkspace private readonly agentSessionsWorkspace: IAgentSessionsWorkspace,32@IChatSessionMetadataStore private readonly metadataStore: IChatSessionMetadataStore,33@IChatSessionWorkspaceFolderService private readonly workspaceFolderService: IChatSessionWorkspaceFolderService,34@IChatSessionWorktreeService private readonly worktreeService: IChatSessionWorktreeService,35@IGitService private readonly gitService: IGitService,36@ILogService private readonly logService: ILogService,37@IVSCodeExtensionContext private readonly extensionContext: IVSCodeExtensionContext,38) {39super();40}4142async handleRequest(sessionId: string): Promise<void> {43if (!this._getSessionCheckpointSupport()) {44this.logService.trace('[ChatSessionWorktreeCheckpointService][handleRequest] Session does not support checkpoints, skipping baseline checkpoint creation');45return;46}4748const repositoryUri = await this._getSessionRepository(sessionId);49const repository = repositoryUri ? await this.gitService.getRepository(repositoryUri) : undefined;5051if (!repository || !repository.headCommitHash) {52this.logService.warn(`[ChatSessionWorktreeCheckpointService][handleRequest] No repository found for session ${sessionId}, skipping baseline checkpoint creation`);53return;54}5556// Initialize checkpoint state and capture baseline checkpoint57const checkpointRef = await this._createCheckpoint(sessionId, repository, 0);58if (!checkpointRef) {59return;60}6162// Update session metadata63const worktreeProperties = await this.worktreeService.getWorktreeProperties(sessionId);64if (!worktreeProperties || typeof worktreeProperties === 'string' || worktreeProperties.version === 1) {65this.logService.trace(`[ChatSessionWorktreeCheckpointService][handleRequest] Session ${sessionId} does not use a git worktree, skipping checkpoint metadata update`);66return;67}6869await this.worktreeService.setWorktreeProperties(sessionId, {70...worktreeProperties,71firstCheckpointRef: checkpointRef,72baseCheckpointRef: checkpointRef,73lastCheckpointRef: checkpointRef74});75}7677async handleRequestCompleted(sessionId: string, requestId: string): Promise<void> {78if (!this._getSessionCheckpointSupport()) {79this.logService.trace('[ChatSessionWorktreeCheckpointService][handleRequestCompleted] Session does not support checkpoints, skipping post-turn checkpoint');80return;81}8283const repositoryUri = await this._getSessionRepository(sessionId);84const repository = repositoryUri ? await this.gitService.getRepository(repositoryUri) : undefined;8586if (!repository || !repository.headCommitHash) {87this.logService.warn(`[ChatSessionWorktreeCheckpointService][handleRequestCompleted] No repository found for session ${sessionId}, skipping post-turn checkpoint`);88return;89}9091const parentCheckpointRef = await this._getLatestCheckpointRef(sessionId);92if (!parentCheckpointRef) {93this.logService.warn(`[ChatSessionWorktreeCheckpointService][handleRequestCompleted] No existing checkpoint ref found for session ${sessionId} on request completion, skipping post-turn checkpoint`);94return;95}9697// Create checkpoint98const currentTurn = parseInt(parentCheckpointRef.split('/').pop() ?? '0') + 1;99const checkpointRef = await this._createCheckpoint(sessionId, repository, currentTurn, parentCheckpointRef);100if (!checkpointRef) {101return;102}103104const worktreeProperties = await this.worktreeService.getWorktreeProperties(sessionId);105if (worktreeProperties && typeof worktreeProperties !== 'string' && worktreeProperties.version === 2) {106// Worktree isolation mode107await this.worktreeService.setWorktreeProperties(sessionId, {108...worktreeProperties,109changes: undefined,110lastCheckpointRef: checkpointRef111});112}113114// Update request metadata with new checkpoint ref115await this.metadataStore.updateRequestDetails(sessionId, [{ vscodeRequestId: requestId, checkpointRef }]);116}117118private async _getSessionRepository(sessionId: string): Promise<Uri | undefined> {119const worktreeProperties = await this.worktreeService.getWorktreeProperties(sessionId);120if (worktreeProperties) {121// Worktree isolation mode122if (typeof worktreeProperties === 'string' || worktreeProperties.version === 1) {123return undefined;124}125126return Uri.file(worktreeProperties.worktreePath);127}128129// Workspace isolation mode130return this.workspaceFolderService.getSessionWorkspaceFolder(sessionId);131}132133private async _getLatestCheckpointRef(sessionId: string): Promise<string | undefined> {134const repositoryUri = await this._getSessionRepository(sessionId);135const repository = repositoryUri ? await this.gitService.getRepository(repositoryUri) : undefined;136if (!repository) {137return undefined;138}139140try {141const refPattern = `${CHECKPOINT_REF_PREFIX}${sessionId}/checkpoints/turn/`;142const refs = await this.gitService.exec(repository.rootUri, [143'for-each-ref', '--sort=-committerdate', '--format=%(refname)', refPattern]);144145return refs ? refs.split('\n')[0] : undefined;146} catch (error) {147this.logService.error(`[ChatSessionWorktreeCheckpointService][_getLatestCheckpointRef] Failed to get latest checkpoint ref for session ${sessionId}: `, error);148return undefined;149}150}151152private _getSessionCheckpointSupport(): boolean {153return this.agentSessionsWorkspace.isAgentSessionsWorkspace;154}155156async handleAdditionalWorktreesRequest(sessionId: string): Promise<void> {157if (!this._getSessionCheckpointSupport()) {158return;159}160161const additionalProps = await this.worktreeService.getAdditionalWorktreeProperties(sessionId);162for (const props of additionalProps) {163if (typeof props === 'string' || props.version === 1) {164continue;165}166const repoUri = Uri.file(props.worktreePath);167const repository = await this.gitService.getRepository(repoUri);168if (!repository || !repository.headCommitHash) {169this.logService.warn(`[ChatSessionWorktreeCheckpointService][handleAdditionalWorktreesRequest] No repository found for additional worktree ${props.worktreePath}`);170continue;171}172await this._createCheckpoint(sessionId, repository, 0);173}174}175176async handleAdditionalWorktreesRequestCompleted(sessionId: string, requestId: string): Promise<void> {177if (!this._getSessionCheckpointSupport()) {178return;179}180181const additionalProps = await this.worktreeService.getAdditionalWorktreeProperties(sessionId);182const additionalCheckpointRefs: { [folderPath: string]: string } = {};183184await Promise.allSettled(additionalProps.map(async (props) => {185if (typeof props === 'string' || props.version === 1) {186return;187}188const repoUri = Uri.file(props.worktreePath);189const repository = await this.gitService.getRepository(repoUri);190if (!repository || !repository.headCommitHash) {191return;192}193194const parentCheckpointRef = await this._getLatestCheckpointRef(sessionId);195const currentTurn = parentCheckpointRef ? parseInt(parentCheckpointRef.split('/').pop() ?? '0') + 1 : 0;196const checkpointRef = await this._createCheckpoint(sessionId, repository, currentTurn, parentCheckpointRef);197if (checkpointRef) {198additionalCheckpointRefs[props.repositoryPath] = checkpointRef;199}200}));201202if (Object.keys(additionalCheckpointRefs).length > 0) {203await this.metadataStore.updateRequestDetails(sessionId, [{ vscodeRequestId: requestId, additionalCheckpointRefs }]);204}205}206207private async _createCheckpoint(sessionId: string, repository: RepoContext, turnNumber: number, parentCheckpointRef?: string): Promise<string | undefined> {208const repositoryUri = repository.rootUri;209210const tmpDirName = `vscode-sessions-${sessionId}-${generateUuid()}`;211const checkpointIndexFile = path.join(this.extensionContext.globalStorageUri.fsPath, tmpDirName, `checkpoint.index`);212const pathspecFile = path.join(this.extensionContext.globalStorageUri.fsPath, tmpDirName, `pathspec.txt`);213214const env = buildTempIndexEnv(repository, checkpointIndexFile);215216try {217// Create temp index file directory218await fs.mkdir(path.dirname(checkpointIndexFile), { recursive: true });219220// Populate temp index from HEAD221await this.gitService.exec(repositoryUri, ['read-tree', 'HEAD'], env);222223// Stage entire working directory into temp index224const uncommittedFilePaths = getUncommittedFilePaths(repository);225await fs.writeFile(pathspecFile, uncommittedFilePaths.join('\n'), 'utf8');226await this.gitService.exec(repositoryUri, ['add', '-A', `--pathspec-from-file=${pathspecFile}`], env);227228// Write the temp index as a tree object229const treeOid = await this.gitService.exec(repositoryUri, ['write-tree'], env);230231// Resolve parent checkpoint ref232const parentCommitOid = parentCheckpointRef233? await this.gitService.exec(repositoryUri, ['rev-parse', parentCheckpointRef])234: undefined;235236// Create a commit pointing to the tree, chained to the previous checkpoint237const commitTreeArgs = ['commit-tree', treeOid, ...(parentCommitOid ? ['-p', parentCommitOid] : []), '-m', `Session ${sessionId} - checkpoint turn ${turnNumber}`];238const commitOid = await this.gitService.exec(repositoryUri, commitTreeArgs);239240// Point a new ref at the commit241const checkpointRef = getCheckpointRef(sessionId, turnNumber);242await this.gitService.exec(repositoryUri, ['update-ref', checkpointRef, commitOid]);243244this.logService.trace(`[ChatSessionWorktreeCheckpointService][_createCheckpoint] Captured checkpoint turn ${turnNumber} for session ${sessionId} at ${checkpointRef}`);245return checkpointRef;246} catch (error) {247this.logService.error(`[ChatSessionWorktreeCheckpointService][_createCheckpoint] Failed to capture checkpoint turn ${turnNumber} for session ${sessionId}: `, error);248return undefined;249} finally {250try {251await fs.rm(path.dirname(checkpointIndexFile), { recursive: true, force: true });252} catch (error) {253this.logService.error(`[ChatSessionWorktreeCheckpointService][_createCheckpoint] Error while cleaning up temp index file for session ${sessionId}: ${error}`);254}255}256}257}258259260