diff --git a/extensions/git/package.json b/extensions/git/package.json index 300ead64450..8a003bdefb4 100644 --- a/extensions/git/package.json +++ b/extensions/git/package.json @@ -551,6 +551,12 @@ "category": "Git", "enablement": "!operationInProgress" }, + { + "command": "git.migrateWorktreeChanges", + "title": "%command.migrateWorktreeChanges%", + "category": "Git", + "enablement": "!operationInProgress" + }, { "command": "git.createWorktree", "title": "%command.createWorktree%", @@ -1327,6 +1333,10 @@ "command": "git.deleteTag", "when": "config.git.enabled && !git.missing && gitOpenRepositoryCount != 0" }, + { + "command": "git.migrateWorktreeChanges", + "when": "config.git.enabled && !git.missing && gitOpenRepositoryCount != 0" + }, { "command": "git.createWorktree", "when": "config.git.enabled && !git.missing && gitOpenRepositoryCount != 0" diff --git a/extensions/git/package.nls.json b/extensions/git/package.nls.json index 07204a1dd34..8e2d58ee7a2 100644 --- a/extensions/git/package.nls.json +++ b/extensions/git/package.nls.json @@ -77,6 +77,7 @@ "command.rebase": "Rebase Branch...", "command.createTag": "Create Tag...", "command.deleteTag": "Delete Tag...", + "command.migrateWorktreeChanges": "Migrate Worktree Changes...", "command.createWorktree": "Create Worktree...", "command.deleteWorktree": "Delete Worktree", "command.deleteWorktreeFromPalette": "Delete Worktree...", diff --git a/extensions/git/src/commands.ts b/extensions/git/src/commands.ts index c18679403f8..1039e021ac5 100644 --- a/extensions/git/src/commands.ts +++ b/extensions/git/src/commands.ts @@ -3407,6 +3407,77 @@ export class CommandCenter { } } + @command('git.migrateWorktreeChanges', { repository: true, repositoryFilter: ['repository', 'submodule'] }) + async migrateWorktreeChanges(repository: Repository): Promise { + const worktreePicks = async (): Promise => { + const worktrees = await repository.getWorktrees(); + return worktrees.length === 0 + ? [{ label: l10n.t('$(info) This repository has no worktrees.') }] + : worktrees.map(worktree => new WorktreeItem(worktree)); + }; + + const placeHolder = l10n.t('Select a worktree to migrate changes from'); + const choice = await this.pickRef(worktreePicks(), placeHolder); + + if (!choice || !(choice instanceof WorktreeItem)) { + return; + } + + const worktreeRepository = this.model.getRepository(choice.worktree.path); + if (!worktreeRepository) { + return; + } + + if (worktreeRepository.indexGroup.resourceStates.length === 0 && + worktreeRepository.workingTreeGroup.resourceStates.length === 0 && + worktreeRepository.untrackedGroup.resourceStates.length === 0) { + await window.showInformationMessage(l10n.t('There are no changes in the selected worktree to migrate.')); + return; + } + + const worktreeChangedFilePaths = [ + ...worktreeRepository.indexGroup.resourceStates, + ...worktreeRepository.workingTreeGroup.resourceStates, + ...worktreeRepository.untrackedGroup.resourceStates + ].map(resource => path.relative(worktreeRepository.root, resource.resourceUri.fsPath)); + + const targetChangedFilePaths = [ + ...repository.workingTreeGroup.resourceStates, + ...repository.untrackedGroup.resourceStates + ].map(resource => path.relative(repository.root, resource.resourceUri.fsPath)); + + // Detect overlapping unstaged files in worktree stash and target repository + const conflicts = worktreeChangedFilePaths.filter(path => targetChangedFilePaths.includes(path)); + + // Check for 'LocalChangesOverwritten' error + if (conflicts.length > 0) { + const fileList = conflicts.join('\n '); + const message = l10n.t('Your local changes to the following files would be overwritten by merge:\n {0}\n\nPlease stage, commit, or stash your changes in the repository before migrating changes.', fileList); + await window.showErrorMessage(message, { modal: true }); + return; + } + + await worktreeRepository.createStash(undefined, true); + const stashes = await worktreeRepository.getStashes(); + + try { + await repository.applyStash(stashes[0].index); + worktreeRepository.dropStash(stashes[0].index); + } catch (err) { + if (err.gitErrorCode !== GitErrorCodes.StashConflict) { + await worktreeRepository.popStash(); + throw err; + } + const message = l10n.t('There are merge conflicts from migrating changes. Please resolve them before committing.'); + const show = l10n.t('Show Changes'); + const choice = await window.showWarningMessage(message, show); + if (choice === show) { + await commands.executeCommand('workbench.view.scm'); + } + worktreeRepository.dropStash(stashes[0].index); + } + } + @command('git.createWorktree') async createWorktree(repository: any): Promise { repository = this.model.getRepository(repository);