mirror of
https://github.com/microsoft/vscode.git
synced 2025-12-24 12:19:20 +00:00
Git - refactor migrate changes functionality (#278426)
* Git - rework migrate changes * Add extension API * Revert some of the options * Remove staged option * More cleanup * More command cleanup
This commit is contained in:
@@ -326,6 +326,10 @@ export class ApiRepository implements Repository {
|
||||
deleteWorktree(path: string, options?: { force?: boolean }): Promise<void> {
|
||||
return this.#repository.deleteWorktree(path, options);
|
||||
}
|
||||
|
||||
migrateChanges(sourceRepositoryPath: string, options?: { confirmation?: boolean; deleteFromSource?: boolean; untracked?: boolean }): Promise<void> {
|
||||
return this.#repository.migrateChanges(sourceRepositoryPath, options);
|
||||
}
|
||||
}
|
||||
|
||||
export class ApiGit implements Git {
|
||||
|
||||
2
extensions/git/src/api/git.d.ts
vendored
2
extensions/git/src/api/git.d.ts
vendored
@@ -292,6 +292,8 @@ export interface Repository {
|
||||
|
||||
createWorktree(options?: { path?: string; commitish?: string; branch?: string }): Promise<string>;
|
||||
deleteWorktree(path: string, options?: { force?: boolean }): Promise<void>;
|
||||
|
||||
migrateChanges(sourceRepositoryPath: string, options?: { confirmation?: boolean; deleteFromSource?: boolean; untracked?: boolean }): Promise<void>;
|
||||
}
|
||||
|
||||
export interface RemoteSource {
|
||||
|
||||
@@ -3383,103 +3383,38 @@ export class CommandCenter {
|
||||
}
|
||||
|
||||
@command('git.migrateWorktreeChanges', { repository: true, repositoryFilter: ['repository', 'submodule'] })
|
||||
async migrateWorktreeChanges(repository: Repository, worktreeUri?: Uri): Promise<void> {
|
||||
async migrateWorktreeChanges(repository: Repository): Promise<void> {
|
||||
let worktreeRepository: Repository | undefined;
|
||||
if (worktreeUri !== undefined) {
|
||||
worktreeRepository = this.model.getRepository(worktreeUri);
|
||||
|
||||
const worktrees = await repository.getWorktrees();
|
||||
if (worktrees.length === 1) {
|
||||
worktreeRepository = this.model.getRepository(worktrees[0].path);
|
||||
} else {
|
||||
const worktrees = await repository.getWorktrees();
|
||||
if (worktrees.length === 1) {
|
||||
worktreeRepository = this.model.getRepository(worktrees[0].path);
|
||||
} else {
|
||||
const worktreePicks = async (): Promise<WorktreeItem[] | QuickPickItem[]> => {
|
||||
return worktrees.length === 0
|
||||
? [{ label: l10n.t('$(info) This repository has no worktrees.') }]
|
||||
: worktrees.map(worktree => new WorktreeItem(worktree));
|
||||
};
|
||||
const worktreePicks = async (): Promise<WorktreeItem[] | QuickPickItem[]> => {
|
||||
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<WorktreeItem | QuickPickItem>(worktreePicks(), placeHolder);
|
||||
const placeHolder = l10n.t('Select a worktree to migrate changes from');
|
||||
const choice = await this.pickRef<WorktreeItem | QuickPickItem>(worktreePicks(), placeHolder);
|
||||
|
||||
if (!choice || !(choice instanceof WorktreeItem)) {
|
||||
return;
|
||||
}
|
||||
|
||||
worktreeRepository = this.model.getRepository(choice.worktree.path);
|
||||
if (!choice || !(choice instanceof WorktreeItem)) {
|
||||
return;
|
||||
}
|
||||
|
||||
worktreeRepository = this.model.getRepository(choice.worktree.path);
|
||||
}
|
||||
|
||||
if (!worktreeRepository || worktreeRepository.kind !== 'worktree') {
|
||||
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 maxFilesShown = 5;
|
||||
const filesToShow = conflicts.slice(0, maxFilesShown);
|
||||
const remainingCount = conflicts.length - maxFilesShown;
|
||||
|
||||
const fileList = filesToShow.join('\n ') +
|
||||
(remainingCount > 0 ? l10n.t('\n and {0} more file{1}...', remainingCount, remainingCount > 1 ? 's' : '') : '');
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
if (worktreeUri === undefined) {
|
||||
// Non-interactive migration, do not show confirmation dialog
|
||||
const message = l10n.t('Proceed with migrating changes to the current repository?');
|
||||
const detail = l10n.t('This will apply the worktree\'s changes to this repository and discard changes in the worktree.\nThis is IRREVERSIBLE!');
|
||||
const proceed = l10n.t('Proceed');
|
||||
const pick = await window.showWarningMessage(message, { modal: true, detail }, proceed);
|
||||
if (pick !== proceed) {
|
||||
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;
|
||||
}
|
||||
repository.isWorktreeMigrating = true;
|
||||
|
||||
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);
|
||||
}
|
||||
await repository.migrateChanges(worktreeRepository.root, {
|
||||
confirmation: true,
|
||||
deleteFromSource: false,
|
||||
untracked: true
|
||||
});
|
||||
}
|
||||
|
||||
@command('git.openWorktreeMergeEditor')
|
||||
|
||||
@@ -2448,6 +2448,90 @@ export class Repository implements Disposable {
|
||||
}
|
||||
}
|
||||
|
||||
async migrateChanges(sourceRepositoryRoot: string, options?: { confirmation?: boolean; deleteFromSource?: boolean; untracked?: boolean }): Promise<void> {
|
||||
const sourceRepository = this.repositoryResolver.getRepository(sourceRepositoryRoot);
|
||||
if (!sourceRepository) {
|
||||
window.showWarningMessage(l10n.t('The source repository could not be found.'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (sourceRepository.indexGroup.resourceStates.length === 0 &&
|
||||
sourceRepository.workingTreeGroup.resourceStates.length === 0 &&
|
||||
sourceRepository.untrackedGroup.resourceStates.length === 0) {
|
||||
await window.showInformationMessage(l10n.t('There are no changes in the selected worktree to migrate.'));
|
||||
return;
|
||||
}
|
||||
|
||||
const sourceFilePaths = [
|
||||
...sourceRepository.indexGroup.resourceStates,
|
||||
...sourceRepository.workingTreeGroup.resourceStates,
|
||||
...sourceRepository.untrackedGroup.resourceStates
|
||||
].map(resource => path.relative(sourceRepository.root, resource.resourceUri.fsPath));
|
||||
|
||||
const targetFilePaths = [
|
||||
...this.workingTreeGroup.resourceStates,
|
||||
...this.untrackedGroup.resourceStates
|
||||
].map(resource => path.relative(this.root, resource.resourceUri.fsPath));
|
||||
|
||||
// Detect overlapping unstaged files in worktree stash and target repository
|
||||
const conflicts = sourceFilePaths.filter(path => targetFilePaths.includes(path));
|
||||
|
||||
if (conflicts.length > 0) {
|
||||
const maxFilesShown = 5;
|
||||
const filesToShow = conflicts.slice(0, maxFilesShown);
|
||||
const remainingCount = conflicts.length - maxFilesShown;
|
||||
|
||||
const fileList = filesToShow.join('\n ') +
|
||||
(remainingCount > 0 ? l10n.t('\n and {0} more file{1}...', remainingCount, remainingCount > 1 ? 's' : '') : '');
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
if (options?.confirmation) {
|
||||
// Non-interactive migration, do not show confirmation dialog
|
||||
const message = l10n.t('Proceed with migrating changes to the current repository?');
|
||||
const detail = l10n.t('This will apply the worktree\'s changes to this repository and discard changes in the worktree.\nThis is IRREVERSIBLE!');
|
||||
const proceed = l10n.t('Proceed');
|
||||
const pick = await window.showWarningMessage(message, { modal: true, detail }, proceed);
|
||||
if (pick !== proceed) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const stashName = `migration-${sourceRepository.HEAD?.name ?? sourceRepository.HEAD?.commit}-${this.HEAD?.name ?? this.HEAD?.commit}`;
|
||||
await sourceRepository.createStash(stashName, options?.untracked);
|
||||
const stashes = await sourceRepository.getStashes();
|
||||
|
||||
try {
|
||||
await this.applyStash(stashes[0].index);
|
||||
|
||||
if (options?.deleteFromSource) {
|
||||
await sourceRepository.dropStash(stashes[0].index);
|
||||
} else {
|
||||
await sourceRepository.popStash();
|
||||
}
|
||||
} catch (err) {
|
||||
if (err.gitErrorCode === GitErrorCodes.StashConflict) {
|
||||
this.isWorktreeMigrating = true;
|
||||
|
||||
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');
|
||||
}
|
||||
|
||||
await sourceRepository.popStash();
|
||||
return;
|
||||
}
|
||||
|
||||
await sourceRepository.popStash();
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
private async retryRun<T>(operation: Operation, runOperation: () => Promise<T>): Promise<T> {
|
||||
let attempt = 0;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user