mirror of
https://github.com/microsoft/vscode.git
synced 2026-04-17 15:24:40 +01:00
Merge pull request #299090 from microsoft/osortega/interior-flea
Session window: merge to local
This commit is contained in:
@@ -1060,6 +1060,22 @@ export class CommandCenter {
|
||||
await repo.pull();
|
||||
}
|
||||
|
||||
@command('_git.revParseAbbrevRef')
|
||||
async revParseAbbrevRef(repositoryPath: string): Promise<string> {
|
||||
const dotGit = await this.git.getRepositoryDotGit(repositoryPath);
|
||||
const repo = new GitRepository(this.git, repositoryPath, undefined, dotGit, this.logger);
|
||||
const result = await repo.exec(['rev-parse', '--abbrev-ref', 'HEAD']);
|
||||
return result.stdout.trim();
|
||||
}
|
||||
|
||||
@command('_git.mergeBranch')
|
||||
async mergeBranch(repositoryPath: string, branch: string): Promise<string> {
|
||||
const dotGit = await this.git.getRepositoryDotGit(repositoryPath);
|
||||
const repo = new GitRepository(this.git, repositoryPath, undefined, dotGit, this.logger);
|
||||
const result = await repo.exec(['merge', branch, '--no-edit']);
|
||||
return result.stdout.trim();
|
||||
}
|
||||
|
||||
@command('git.init')
|
||||
async init(skipFolderPrompt = false): Promise<void> {
|
||||
let repositoryPath: string | undefined = undefined;
|
||||
@@ -5595,15 +5611,14 @@ export class CommandCenter {
|
||||
options.modal = false;
|
||||
break;
|
||||
default: {
|
||||
const hint = (err.stderr || err.message || String(err))
|
||||
const hintLines = (err.stderr || err.stdout || err.message || String(err))
|
||||
.replace(/^error: /mi, '')
|
||||
.replace(/^> husky.*$/mi, '')
|
||||
.split(/[\r\n]/)
|
||||
.filter((line: string) => !!line)
|
||||
[0];
|
||||
.filter((line: string) => !!line);
|
||||
|
||||
message = hint
|
||||
? l10n.t('Git: {0}', hint)
|
||||
message = hintLines.length > 0
|
||||
? l10n.t('Git: {0}', err.stdout ? hintLines[hintLines.length - 1] : hintLines[0])
|
||||
: l10n.t('Git error');
|
||||
|
||||
break;
|
||||
|
||||
@@ -183,7 +183,7 @@
|
||||
"when": "github.hasGitHubRepo && timelineItem =~ /git:file:commit\\b/"
|
||||
}
|
||||
],
|
||||
"chat/input/editing/sessionToolbar": [
|
||||
"chat/input/editing/sessionApplyActions": [
|
||||
{
|
||||
"command": "github.createPullRequest",
|
||||
"group": "navigation",
|
||||
|
||||
@@ -259,6 +259,7 @@ export class MenuId {
|
||||
static readonly ChatModePicker = new MenuId('ChatModePicker');
|
||||
static readonly ChatEditingWidgetToolbar = new MenuId('ChatEditingWidgetToolbar');
|
||||
static readonly ChatEditingSessionChangesToolbar = new MenuId('ChatEditingSessionChangesToolbar');
|
||||
static readonly ChatEditingSessionApplySubmenu = new MenuId('ChatEditingSessionApplySubmenu');
|
||||
static readonly ChatEditingEditorContent = new MenuId('ChatEditingEditorContent');
|
||||
static readonly ChatEditingEditorHunk = new MenuId('ChatEditingEditorHunk');
|
||||
static readonly ChatEditingDeletedNotebookCell = new MenuId('ChatEditingDeletedNotebookCell');
|
||||
|
||||
@@ -9,40 +9,29 @@ import { Schemas } from '../../../../base/common/network.js';
|
||||
import { autorun } from '../../../../base/common/observable.js';
|
||||
import { Codicon } from '../../../../base/common/codicons.js';
|
||||
import { localize, localize2 } from '../../../../nls.js';
|
||||
import { Action2, MenuId, registerAction2 } from '../../../../platform/actions/common/actions.js';
|
||||
import { Action2, MenuId, MenuRegistry, registerAction2 } from '../../../../platform/actions/common/actions.js';
|
||||
import { ICommandService } from '../../../../platform/commands/common/commands.js';
|
||||
import { ContextKeyExpr, IContextKeyService, RawContextKey } from '../../../../platform/contextkey/common/contextkey.js';
|
||||
import { ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js';
|
||||
import { IFileService } from '../../../../platform/files/common/files.js';
|
||||
import { INotificationService, Severity } from '../../../../platform/notification/common/notification.js';
|
||||
import { IOpenerService } from '../../../../platform/opener/common/opener.js';
|
||||
import { IProductService } from '../../../../platform/product/common/productService.js';
|
||||
import { IWorkbenchContribution, registerWorkbenchContribution2, WorkbenchPhase } from '../../../../workbench/common/contributions.js';
|
||||
import { IsSessionsWindowContext } from '../../../../workbench/common/contextkeys.js';
|
||||
import { CHAT_CATEGORY } from '../../../../workbench/contrib/chat/browser/actions/chatActions.js';
|
||||
import { ChatContextKeys } from '../../../../workbench/contrib/chat/common/actions/chatContextKeys.js';
|
||||
import { IAgentSessionsService } from '../../../../workbench/contrib/chat/browser/agentSessions/agentSessionsService.js';
|
||||
import { isIChatSessionFileChange2 } from '../../../../workbench/contrib/chat/common/chatSessionsService.js';
|
||||
import { ISessionsManagementService } from '../../sessions/browser/sessionsManagementService.js';
|
||||
import { IsSessionsWindowContext } from '../../../../workbench/common/contextkeys.js';
|
||||
import { isEqualOrParent, joinPath, relativePath } from '../../../../base/common/resources.js';
|
||||
import { ILogService } from '../../../../platform/log/common/log.js';
|
||||
import { URI } from '../../../../base/common/uri.js';
|
||||
|
||||
/**
|
||||
* Normalizes a URI to the `file` scheme so that path comparisons work
|
||||
* even when the source URI uses a different scheme (e.g. `github-remote-file`).
|
||||
*/
|
||||
function toFileUri(uri: URI): URI {
|
||||
return uri.scheme === 'file' ? uri : URI.file(uri.path);
|
||||
}
|
||||
|
||||
const hasWorktreeAndRepositoryContextKey = new RawContextKey<boolean>('agentSessionHasWorktreeAndRepository', false, {
|
||||
type: 'boolean',
|
||||
description: localize('agentSessionHasWorktreeAndRepository', "True when the active agent session has both a worktree and a parent repository.")
|
||||
});
|
||||
|
||||
class ApplyToParentRepoContribution extends Disposable implements IWorkbenchContribution {
|
||||
class ApplyChangesToParentRepoContribution extends Disposable implements IWorkbenchContribution {
|
||||
|
||||
static readonly ID = 'sessions.contrib.applyToParentRepo';
|
||||
static readonly ID = 'sessions.contrib.applyChangesToParentRepo';
|
||||
|
||||
constructor(
|
||||
@IContextKeyService contextKeyService: IContextKeyService,
|
||||
@@ -50,32 +39,38 @@ class ApplyToParentRepoContribution extends Disposable implements IWorkbenchCont
|
||||
) {
|
||||
super();
|
||||
|
||||
const contextKey = hasWorktreeAndRepositoryContextKey.bindTo(contextKeyService);
|
||||
const worktreeAndRepoKey = hasWorktreeAndRepositoryContextKey.bindTo(contextKeyService);
|
||||
|
||||
this._register(autorun(reader => {
|
||||
const activeSession = sessionManagementService.activeSession.read(reader);
|
||||
const hasWorktreeAndRepo = !!activeSession?.worktree && !!activeSession?.repository;
|
||||
contextKey.set(hasWorktreeAndRepo);
|
||||
worktreeAndRepoKey.set(hasWorktreeAndRepo);
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
class ApplyToParentRepoAction extends Action2 {
|
||||
static readonly ID = 'chatEditing.applyToParentRepo';
|
||||
class ApplyChangesToParentRepoAction extends Action2 {
|
||||
static readonly ID = 'chatEditing.applyChangesToParentRepo';
|
||||
|
||||
constructor() {
|
||||
super({
|
||||
id: ApplyToParentRepoAction.ID,
|
||||
title: localize2('applyToParentRepo', 'Apply to Parent Repo'),
|
||||
id: ApplyChangesToParentRepoAction.ID,
|
||||
title: localize2('applyChangesToParentRepo', 'Apply Changes to Parent Repository'),
|
||||
icon: Codicon.desktopDownload,
|
||||
category: CHAT_CATEGORY,
|
||||
precondition: ContextKeyExpr.and(IsSessionsWindowContext, hasWorktreeAndRepositoryContextKey, ChatContextKeys.hasAgentSessionChanges),
|
||||
precondition: ContextKeyExpr.and(
|
||||
IsSessionsWindowContext,
|
||||
hasWorktreeAndRepositoryContextKey,
|
||||
),
|
||||
menu: [
|
||||
{
|
||||
id: MenuId.ChatEditingSessionChangesToolbar,
|
||||
id: MenuId.ChatEditingSessionApplySubmenu,
|
||||
group: 'navigation',
|
||||
order: 4,
|
||||
when: ContextKeyExpr.and(IsSessionsWindowContext, hasWorktreeAndRepositoryContextKey, ChatContextKeys.hasAgentSessionChanges),
|
||||
order: 2,
|
||||
when: ContextKeyExpr.and(
|
||||
IsSessionsWindowContext,
|
||||
hasWorktreeAndRepositoryContextKey,
|
||||
),
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -83,8 +78,7 @@ class ApplyToParentRepoAction extends Action2 {
|
||||
|
||||
override async run(accessor: ServicesAccessor): Promise<void> {
|
||||
const sessionManagementService = accessor.get(ISessionsManagementService);
|
||||
const agentSessionsService = accessor.get(IAgentSessionsService);
|
||||
const fileService = accessor.get(IFileService);
|
||||
const commandService = accessor.get(ICommandService);
|
||||
const notificationService = accessor.get(INotificationService);
|
||||
const logService = accessor.get(ILogService);
|
||||
const openerService = accessor.get(IOpenerService);
|
||||
@@ -98,55 +92,8 @@ class ApplyToParentRepoAction extends Action2 {
|
||||
const worktreeRoot = activeSession.worktree;
|
||||
const repoRoot = activeSession.repository;
|
||||
|
||||
const agentSession = agentSessionsService.getSession(activeSession.resource);
|
||||
const changes = agentSession?.changes;
|
||||
if (!changes || !(changes instanceof Array)) {
|
||||
return;
|
||||
}
|
||||
|
||||
let copiedCount = 0;
|
||||
let deletedCount = 0;
|
||||
let errorCount = 0;
|
||||
|
||||
for (const change of changes) {
|
||||
try {
|
||||
const modifiedUri = isIChatSessionFileChange2(change)
|
||||
? change.modifiedUri ?? change.uri
|
||||
: change.modifiedUri;
|
||||
const isDeletion = isIChatSessionFileChange2(change)
|
||||
? change.modifiedUri === undefined
|
||||
: false;
|
||||
|
||||
if (isDeletion) {
|
||||
const originalUri = change.originalUri;
|
||||
if (originalUri && isEqualOrParent(toFileUri(originalUri), worktreeRoot)) {
|
||||
const relPath = relativePath(worktreeRoot, toFileUri(originalUri));
|
||||
if (relPath) {
|
||||
const targetUri = joinPath(repoRoot, relPath);
|
||||
if (await fileService.exists(targetUri)) {
|
||||
await fileService.del(targetUri);
|
||||
deletedCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (isEqualOrParent(toFileUri(modifiedUri), worktreeRoot)) {
|
||||
const relPath = relativePath(worktreeRoot, toFileUri(modifiedUri));
|
||||
if (relPath) {
|
||||
const targetUri = joinPath(repoRoot, relPath);
|
||||
await fileService.copy(modifiedUri, targetUri, true);
|
||||
copiedCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
logService.error('[ApplyToParentRepo] Failed to apply change', err);
|
||||
errorCount++;
|
||||
}
|
||||
}
|
||||
|
||||
const openFolderAction = toAction({
|
||||
id: 'applyToParentRepo.openFolder',
|
||||
id: 'applyChangesToParentRepo.openFolder',
|
||||
label: localize('openInVSCode', "Open in VS Code"),
|
||||
run: () => {
|
||||
const scheme = productService.quality === 'stable'
|
||||
@@ -168,26 +115,57 @@ class ApplyToParentRepoAction extends Action2 {
|
||||
}
|
||||
});
|
||||
|
||||
const totalApplied = copiedCount + deletedCount;
|
||||
if (errorCount > 0) {
|
||||
try {
|
||||
// Get the worktree branch name. Since the worktree and parent repo
|
||||
// share the same git object store, the parent can directly reference
|
||||
// this branch for a merge.
|
||||
const worktreeBranch = await commandService.executeCommand<string>(
|
||||
'_git.revParseAbbrevRef',
|
||||
worktreeRoot.fsPath
|
||||
);
|
||||
|
||||
if (!worktreeBranch) {
|
||||
notificationService.notify({
|
||||
severity: Severity.Warning,
|
||||
message: localize('applyChangesNoBranch', "Could not determine worktree branch name."),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Merge the worktree branch into the parent repo.
|
||||
// This is idempotent: if already merged, git says "Already up to date."
|
||||
// If new commits exist, they're brought in. Handles partial applies naturally.
|
||||
const result = await commandService.executeCommand('_git.mergeBranch', repoRoot.fsPath, worktreeBranch);
|
||||
if (!result) {
|
||||
logService.warn('[ApplyChangesToParentRepo] No result from merge command');
|
||||
} else {
|
||||
notificationService.notify({
|
||||
severity: Severity.Info,
|
||||
message: typeof result === 'string' && result.startsWith('Already up to date')
|
||||
? localize('alreadyUpToDate', 'Parent repository is up to date with worktree.')
|
||||
: localize('applyChangesSuccess', 'Applied changes to parent repository.'),
|
||||
actions: { primary: [openFolderAction] }
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
logService.error('[ApplyChangesToParentRepo] Failed to apply changes', err);
|
||||
notificationService.notify({
|
||||
severity: Severity.Warning,
|
||||
message: totalApplied === 1
|
||||
? localize('applyToParentRepoPartial1', "Applied 1 file to parent repo with {0} error(s).", errorCount)
|
||||
: localize('applyToParentRepoPartialN', "Applied {0} files to parent repo with {1} error(s).", totalApplied, errorCount),
|
||||
actions: { primary: [openFolderAction] }
|
||||
});
|
||||
} else if (totalApplied > 0) {
|
||||
notificationService.notify({
|
||||
severity: Severity.Info,
|
||||
message: totalApplied === 1
|
||||
? localize('applyToParentRepoSuccess1', "Applied 1 file to parent repo.")
|
||||
: localize('applyToParentRepoSuccessN', "Applied {0} files to parent repo.", totalApplied),
|
||||
message: localize('applyChangesConflict', "Failed to apply changes to parent repo. The parent repo may have diverged — resolve conflicts manually."),
|
||||
actions: { primary: [openFolderAction] }
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
registerAction2(ApplyToParentRepoAction);
|
||||
registerWorkbenchContribution2(ApplyToParentRepoContribution.ID, ApplyToParentRepoContribution, WorkbenchPhase.AfterRestored);
|
||||
registerAction2(ApplyChangesToParentRepoAction);
|
||||
registerWorkbenchContribution2(ApplyChangesToParentRepoContribution.ID, ApplyChangesToParentRepoContribution, WorkbenchPhase.AfterRestored);
|
||||
|
||||
// Register the apply submenu in the session changes toolbar
|
||||
MenuRegistry.appendMenuItem(MenuId.ChatEditingSessionChangesToolbar, {
|
||||
submenu: MenuId.ChatEditingSessionApplySubmenu,
|
||||
title: localize2('applyActions', 'Apply Actions'),
|
||||
group: 'navigation',
|
||||
order: 1,
|
||||
when: ContextKeyExpr.and(IsSessionsWindowContext, ChatContextKeys.hasAgentSessionChanges),
|
||||
});
|
||||
@@ -593,10 +593,7 @@ export class ChangesViewPane extends ViewPane {
|
||||
return { showIcon: true, showLabel: true, isSecondary: true, customClass: 'working-set-diff-stats', customLabel: diffStatsLabel };
|
||||
}
|
||||
if (action.id === 'github.createPullRequest' || action.id === 'github.openPullRequest') {
|
||||
return { showIcon: true, showLabel: true, isSecondary: true, customClass: 'flex-grow' };
|
||||
}
|
||||
if (action.id === 'chatEditing.applyToParentRepo') {
|
||||
return { showIcon: true, showLabel: false, isSecondary: true };
|
||||
return { showIcon: true, showLabel: true, isSecondary: true };
|
||||
}
|
||||
if (action.id === 'chatEditing.synchronizeChanges') {
|
||||
return { showIcon: true, showLabel: true, isSecondary: true };
|
||||
|
||||
@@ -114,6 +114,29 @@
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
/* ButtonWithDropdown container grows to fill available space */
|
||||
.changes-view-body .chat-editing-session-actions.outside-card .monaco-button-dropdown {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.changes-view-body .chat-editing-session-actions.outside-card .monaco-button-dropdown > .monaco-button {
|
||||
flex: 1;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.changes-view-body .chat-editing-session-actions.outside-card .monaco-button-dropdown > .monaco-button-dropdown-separator {
|
||||
flex: 0;
|
||||
}
|
||||
|
||||
.changes-view-body .chat-editing-session-actions.outside-card .monaco-button-dropdown > .monaco-button.monaco-dropdown-button {
|
||||
flex: 0 0 auto;
|
||||
padding: 4px;
|
||||
width: auto;
|
||||
min-width: 0;
|
||||
border-radius: 0px 4px 4px 0px;
|
||||
}
|
||||
|
||||
.changes-view-body .chat-editing-session-actions.outside-card .monaco-button.secondary.monaco-text-button.codicon {
|
||||
padding: 4px 8px;
|
||||
font-size: 16px !important;
|
||||
@@ -136,6 +159,10 @@
|
||||
color: var(--vscode-button-secondaryForeground);
|
||||
}
|
||||
|
||||
.changes-view-body .chat-editing-session-actions.outside-card .monaco-button-dropdown > .monaco-button.secondary.monaco-text-button {
|
||||
border-radius: 4px 0px 0px 4px;
|
||||
}
|
||||
|
||||
.changes-view-body .chat-editing-session-actions .monaco-button.secondary:hover {
|
||||
background-color: var(--vscode-button-secondaryHoverBackground);
|
||||
color: var(--vscode-button-secondaryForeground);
|
||||
|
||||
@@ -209,7 +209,7 @@ import './contrib/sessions/browser/customizationsToolbar.contribution.js';
|
||||
import './contrib/changesView/browser/changesView.contribution.js';
|
||||
import './contrib/files/browser/files.contribution.js';
|
||||
import './contrib/gitSync/browser/gitSync.contribution.js';
|
||||
import './contrib/applyToParentRepo/browser/applyToParentRepo.contribution.js';
|
||||
import './contrib/applyCommitsToParentRepo/browser/applyChangesToParentRepo.js';
|
||||
import './contrib/fileTreeView/browser/fileTreeView.contribution.js'; // view registration disabled; filesystem provider still needed
|
||||
import './contrib/configuration/browser/configuration.contribution.js';
|
||||
|
||||
|
||||
@@ -109,7 +109,7 @@ function determineChangeType(resource: ISCMResource, groupId: string): 'added' |
|
||||
* files is the presence/absence of a trailing newline (content otherwise identical),
|
||||
* no diff will be generated because VS Code's diff algorithm treats the lines as equal.
|
||||
*/
|
||||
async function generateUnifiedDiff(
|
||||
export async function generateUnifiedDiff(
|
||||
fileService: IFileService,
|
||||
relPath: string,
|
||||
originalUri: URI | undefined,
|
||||
|
||||
@@ -479,6 +479,12 @@ const apiMenus: IAPIMenu[] = [
|
||||
description: localize('menus.chatEditingSessionChangesToolbar', "The Chat Editing widget toolbar menu for session changes."),
|
||||
proposed: 'chatSessionsProvider'
|
||||
},
|
||||
{
|
||||
key: 'chat/input/editing/sessionApplyActions',
|
||||
id: MenuId.ChatEditingSessionApplySubmenu,
|
||||
description: localize('menus.chatEditingSessionApplySubmenu', "Submenu for apply actions in the Chat Editing session changes toolbar."),
|
||||
proposed: 'chatSessionsProvider'
|
||||
},
|
||||
{
|
||||
// TODO: rename this to something like: `chatSessions/item/inline`
|
||||
key: 'chat/chatSessions',
|
||||
|
||||
Reference in New Issue
Block a user