Path: blob/main/src/vs/sessions/contrib/applyCommitsToParentRepo/browser/applyChangesToParentRepo.ts
13579 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 { toAction } from '../../../../base/common/actions.js';6import { Disposable } from '../../../../base/common/lifecycle.js';7import { Schemas } from '../../../../base/common/network.js';8import { autorun } from '../../../../base/common/observable.js';9import { Codicon } from '../../../../base/common/codicons.js';10import { localize, localize2 } from '../../../../nls.js';11import { Action2, MenuId, MenuRegistry, registerAction2 } from '../../../../platform/actions/common/actions.js';12import { ICommandService } from '../../../../platform/commands/common/commands.js';13import { ContextKeyExpr, IContextKeyService, RawContextKey } from '../../../../platform/contextkey/common/contextkey.js';14import { ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js';15import { INotificationService, Severity } from '../../../../platform/notification/common/notification.js';16import { IOpenerService } from '../../../../platform/opener/common/opener.js';17import { IProductService } from '../../../../platform/product/common/productService.js';18import { IWorkbenchContribution, registerWorkbenchContribution2, WorkbenchPhase } from '../../../../workbench/common/contributions.js';19import { IsSessionsWindowContext } from '../../../../workbench/common/contextkeys.js';20import { CHAT_CATEGORY } from '../../../../workbench/contrib/chat/browser/actions/chatActions.js';21import { ISessionsManagementService } from '../../../services/sessions/common/sessionsManagement.js';22import { ILogService } from '../../../../platform/log/common/log.js';23import { URI } from '../../../../base/common/uri.js';2425const hasWorktreeAndRepositoryContextKey = new RawContextKey<boolean>('agentSessionHasWorktreeAndRepository', false, {26type: 'boolean',27description: localize('agentSessionHasWorktreeAndRepository', "True when the active agent session has both a worktree and a parent repository.")28});2930class ApplyChangesToParentRepoContribution extends Disposable implements IWorkbenchContribution {3132static readonly ID = 'sessions.contrib.applyChangesToParentRepo';3334constructor(35@IContextKeyService contextKeyService: IContextKeyService,36@ISessionsManagementService sessionManagementService: ISessionsManagementService,37) {38super();3940const worktreeAndRepoKey = hasWorktreeAndRepositoryContextKey.bindTo(contextKeyService);4142this._register(autorun(reader => {43const activeSession = sessionManagementService.activeSession.read(reader);44const repo = activeSession?.workspace.read(reader)?.repositories[0];45const hasWorktreeAndRepo = !!repo?.workingDirectory && !!repo?.uri;46worktreeAndRepoKey.set(hasWorktreeAndRepo);47}));48}49}5051class ApplyChangesToParentRepoAction extends Action2 {52static readonly ID = 'chatEditing.applyChangesToParentRepo';5354constructor() {55super({56id: ApplyChangesToParentRepoAction.ID,57title: localize2('applyChangesToParentRepo', 'Apply Changes to Parent Repository'),58icon: Codicon.desktopDownload,59category: CHAT_CATEGORY,60precondition: ContextKeyExpr.and(61IsSessionsWindowContext,62hasWorktreeAndRepositoryContextKey,63),64menu: [65{66id: MenuId.ChatEditingSessionApplySubmenu,67group: 'navigation',68order: 2,69when: ContextKeyExpr.and(70ContextKeyExpr.false(),71IsSessionsWindowContext,72hasWorktreeAndRepositoryContextKey73),74},75],76});77}7879override async run(accessor: ServicesAccessor): Promise<void> {80const sessionManagementService = accessor.get(ISessionsManagementService);81const commandService = accessor.get(ICommandService);82const notificationService = accessor.get(INotificationService);83const logService = accessor.get(ILogService);84const openerService = accessor.get(IOpenerService);85const productService = accessor.get(IProductService);8687const activeSession = sessionManagementService.activeSession.get();88const repo = activeSession?.workspace.get()?.repositories[0];89if (!activeSession || !repo?.workingDirectory || !repo?.uri) {90return;91}9293const worktreeRoot = repo.workingDirectory;94const repoRoot = repo.uri;9596const openFolderAction = toAction({97id: 'applyChangesToParentRepo.openFolder',98label: localize('openInVSCode', "Open in VS Code"),99run: () => {100const scheme = productService.quality === 'stable'101? 'vscode'102: productService.quality === 'exploration'103? 'vscode-exploration'104: 'vscode-insiders';105106const params = new URLSearchParams();107params.set('windowId', '_blank');108params.set('session', activeSession.resource.toString());109110openerService.open(URI.from({111scheme,112authority: Schemas.file,113path: repoRoot.path,114query: params.toString(),115}), { openExternal: true });116}117});118119try {120// Get the worktree branch name. Since the worktree and parent repo121// share the same git object store, the parent can directly reference122// this branch for a merge.123const worktreeBranch = await commandService.executeCommand<string>(124'_git.revParseAbbrevRef',125worktreeRoot.fsPath126);127128if (!worktreeBranch) {129notificationService.notify({130severity: Severity.Warning,131message: localize('applyChangesNoBranch', "Could not determine worktree branch name."),132});133return;134}135136// Merge the worktree branch into the parent repo.137// This is idempotent: if already merged, git says "Already up to date."138// If new commits exist, they're brought in. Handles partial applies naturally.139const result = await commandService.executeCommand('_git.mergeBranch', repoRoot.fsPath, worktreeBranch);140if (!result) {141logService.warn('[ApplyChangesToParentRepo] No result from merge command');142} else {143notificationService.notify({144severity: Severity.Info,145message: typeof result === 'string' && result.startsWith('Already up to date')146? localize('alreadyUpToDate', 'Parent repository is up to date with worktree.')147: localize('applyChangesSuccess', 'Applied changes to parent repository.'),148actions: { primary: [openFolderAction] }149});150}151} catch (err) {152logService.error('[ApplyChangesToParentRepo] Failed to apply changes', err);153notificationService.notify({154severity: Severity.Warning,155message: localize('applyChangesConflict', "Failed to apply changes to parent repo. The parent repo may have diverged — resolve conflicts manually."),156actions: { primary: [openFolderAction] }157});158}159}160}161162registerAction2(ApplyChangesToParentRepoAction);163registerWorkbenchContribution2(ApplyChangesToParentRepoContribution.ID, ApplyChangesToParentRepoContribution, WorkbenchPhase.AfterRestored);164165// Register the apply submenu in the session changes toolbar166MenuRegistry.appendMenuItem(MenuId.ChatEditingSessionChangesToolbar, {167submenu: MenuId.ChatEditingSessionApplySubmenu,168title: localize2('applyActions', 'Apply Actions'),169group: 'navigation',170order: 1,171when: IsSessionsWindowContext,172});173174175