Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p
- Changes copying changes to a worktree to no longer prompt to open that worktree afterward
- Changes stashing to use the _Commit Graph_ working changes (WIP) commit-box draft message when one is present
- Renames the _Open Worktree File_ action to _Open File (Worktree)_ for consistency with the new _Open Changes with Working File (Worktree)_ action
- Changes how paused merge, rebase, cherry-pick, and revert operations with conflicts are surfaced — a single surface now opens: the _Commit Graph_'s working changes (WIP) details with the conflict banner, or the _Interactive Rebase Editor_ for rebases when the _Commit Graph_ is unavailable. Previously a rebase conflict could simultaneously open the rebase editor, force-focus the _Commits_ view, and show a toast offering to open the already-opening editor. The _Commits_ view is no longer force-focused by any paused operation, the rebase conflict toasts no longer offer _Open Rebase Editor_, and _Show Conflicts_ now consistently opens the _Commit Graph_ from all locations, including inside the rebase editor; interactive rebase pauses (todo editing, `edit`, `reword`, `break`) still open the rebase editor

### Removed

Expand Down
11 changes: 5 additions & 6 deletions src/commands/git/cherry-pick.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { ensureArray } from '@gitlens/utils/array.js';
import { Logger } from '@gitlens/utils/logger.js';
import { pluralize } from '@gitlens/utils/string.js';
import type { Container } from '../../container.js';
import { skipPausedOperation } from '../../git/actions/pausedOperation.js';
import { showPausedOperationStatus, skipPausedOperation } from '../../git/actions/pausedOperation.js';
import type { GlRepository } from '../../git/models/repository.js';
import { showGitErrorMessage } from '../../messages.js';
import { isSubscriptionTrialOrPaidFromState } from '../../plus/gk/utils/subscription.utils.js';
Expand All @@ -20,7 +20,6 @@ import type { DirectiveQuickPickItem } from '../../quickpicks/items/directive.js
import { createDirectiveQuickPickItem, Directive } from '../../quickpicks/items/directive.js';
import type { FlagsQuickPickItem } from '../../quickpicks/items/flags.js';
import { createFlagsQuickPickItem } from '../../quickpicks/items/flags.js';
import { executeCommand } from '../../system/-webview/command.js';
import type { ViewsWithRepositoryFolders } from '../../views/viewBase.js';
import type {
AsyncStepResultGenerator,
Expand Down Expand Up @@ -98,7 +97,7 @@ export class CherryPickGitCommand extends QuickCommand<State> {
void window.showWarningMessage(
'Unable to cherry-pick due to conflicts. Resolve the conflicts before continuing, or abort the cherry-pick.',
);
void executeCommand('gitlens.showCommitsView');
void showPausedOperationStatus(this.container, state.repo.path);
}
} catch (ex) {
// Don't show an error message if the user intentionally aborted the cherry-pick
Expand All @@ -120,7 +119,7 @@ export class CherryPickGitCommand extends QuickCommand<State> {
void window.showWarningMessage(
'Unable to cherry-pick. A cherry-pick is already in progress. Continue or abort the current cherry-pick first.',
);
void executeCommand('gitlens.showCommitsView');
void showPausedOperationStatus(this.container, state.repo.path);
return;
}

Expand All @@ -146,10 +145,10 @@ export class CherryPickGitCommand extends QuickCommand<State> {
cancel,
);
if (result === skip) {
return void skipPausedOperation(state.repo.git);
return void skipPausedOperation(this.container, state.repo.git);
}

void executeCommand('gitlens.showCommitsView');
void showPausedOperationStatus(this.container, state.repo.path);
return;
}

Expand Down
6 changes: 3 additions & 3 deletions src/commands/git/merge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { createRevisionRange } from '@gitlens/git/utils/revision.utils.js';
import { Logger } from '@gitlens/utils/logger.js';
import { pluralize } from '@gitlens/utils/string.js';
import type { Container } from '../../container.js';
import { showPausedOperationStatus } from '../../git/actions/pausedOperation.js';
import type { GlRepository } from '../../git/models/repository.js';
import { showGitErrorMessage } from '../../messages.js';
import { isSubscriptionTrialOrPaidFromState } from '../../plus/gk/utils/subscription.utils.js';
Expand All @@ -17,7 +18,6 @@ import type { DirectiveQuickPickItem } from '../../quickpicks/items/directive.js
import { createDirectiveQuickPickItem, Directive } from '../../quickpicks/items/directive.js';
import type { FlagsQuickPickItem } from '../../quickpicks/items/flags.js';
import { createFlagsQuickPickItem } from '../../quickpicks/items/flags.js';
import { executeCommand } from '../../system/-webview/command.js';
import type { ViewsWithRepositoryFolders } from '../../views/viewBase.js';
import type {
AsyncStepResultGenerator,
Expand Down Expand Up @@ -106,7 +106,7 @@ export class MergeGitCommand extends QuickCommand<State> {
void window.showWarningMessage(
'Unable to merge due to conflicts. Resolve the conflicts before continuing, or abort the merge.',
);
void executeCommand('gitlens.showCommitsView');
void showPausedOperationStatus(this.container, state.repo.path);
}
} catch (ex) {
// Don't show an error message if the user intentionally aborted the merge
Expand All @@ -128,7 +128,7 @@ export class MergeGitCommand extends QuickCommand<State> {
void window.showWarningMessage(
'Unable to merge. A merge is already in progress. Continue or abort the current merge first.',
);
void executeCommand('gitlens.showCommitsView');
void showPausedOperationStatus(this.container, state.repo.path);
return;
}

Expand Down
40 changes: 10 additions & 30 deletions src/commands/git/rebase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,20 +10,16 @@ import { createDisposable } from '@gitlens/utils/disposable.js';
import { Logger } from '@gitlens/utils/logger.js';
import { pluralize } from '@gitlens/utils/string.js';
import type { Container } from '../../container.js';
import { showPausedOperationStatus } from '../../git/actions/pausedOperation.js';
import type { GlRepository } from '../../git/models/repository.js';
import {
isRebaseTodoEditorEnabled,
openRebaseEditor,
reopenRebaseTodoEditor,
} from '../../git/utils/-webview/rebase.utils.js';
import { isRebaseTodoEditorEnabled, reopenRebaseTodoEditor } from '../../git/utils/-webview/rebase.utils.js';
import { showGitErrorMessage } from '../../messages.js';
import { isSubscriptionTrialOrPaidFromState } from '../../plus/gk/utils/subscription.utils.js';
import { createQuickPickSeparator } from '../../quickpicks/items/common.js';
import type { DirectiveQuickPickItem } from '../../quickpicks/items/directive.js';
import { createDirectiveQuickPickItem, Directive } from '../../quickpicks/items/directive.js';
import type { FlagsQuickPickItem } from '../../quickpicks/items/flags.js';
import { createFlagsQuickPickItem } from '../../quickpicks/items/flags.js';
import { executeCommand } from '../../system/-webview/command.js';
import { getHostEditorCommand } from '../../system/-webview/vscode.js';
import type { ViewsWithRepositoryFolders } from '../../views/viewBase.js';
import type {
Expand Down Expand Up @@ -116,18 +112,10 @@ export class RebaseGitCommand extends QuickCommand<State> {
updateRefs: updateRefs,
});
if (result?.conflicted) {
const openEditor = { title: 'Open Rebase Editor' };
void window
.showWarningMessage(
'Unable to rebase due to conflicts. Resolve the conflicts before continuing, or abort the rebase.',
openEditor,
)
.then(r => {
if (r === openEditor) {
void openRebaseEditor(this.container, state.repo.path);
}
});
void executeCommand('gitlens.showCommitsView');
void window.showWarningMessage(
'Unable to rebase due to conflicts. Resolve the conflicts before continuing, or abort the rebase.',
);
void showPausedOperationStatus(this.container, state.repo.path);
}
} catch (ex) {
// Don't show an error message if the user intentionally aborted the rebase
Expand All @@ -146,18 +134,10 @@ export class RebaseGitCommand extends QuickCommand<State> {
}

if (RebaseError.is(ex, 'alreadyInProgress')) {
const openEditor = { title: 'Open Rebase Editor' };
void window
.showWarningMessage(
'Unable to rebase. A rebase is already in progress. Continue or abort the current rebase first.',
openEditor,
)
.then(result => {
if (result === openEditor) {
void openRebaseEditor(this.container, state.repo.path);
}
});
void executeCommand('gitlens.showCommitsView');
void window.showWarningMessage(
'Unable to rebase. A rebase is already in progress. Continue or abort the current rebase first.',
);
void showPausedOperationStatus(this.container, state.repo.path);
return;
}

Expand Down
6 changes: 3 additions & 3 deletions src/commands/git/revert.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,12 @@ import type { GitRevisionReference } from '@gitlens/git/models/reference.js';
import { getReferenceLabel } from '@gitlens/git/utils/reference.utils.js';
import { Logger } from '@gitlens/utils/logger.js';
import type { Container } from '../../container.js';
import { showPausedOperationStatus } from '../../git/actions/pausedOperation.js';
import type { GlRepository } from '../../git/models/repository.js';
import { showGitErrorMessage } from '../../messages.js';
import { createDirectiveQuickPickItem, Directive } from '../../quickpicks/items/directive.js';
import type { FlagsQuickPickItem } from '../../quickpicks/items/flags.js';
import { createFlagsQuickPickItem } from '../../quickpicks/items/flags.js';
import { executeCommand } from '../../system/-webview/command.js';
import type { ViewsWithRepositoryFolders } from '../../views/viewBase.js';
import type {
PartialStepState,
Expand Down Expand Up @@ -88,7 +88,7 @@ export class RevertGitCommand extends QuickCommand<State> {
void window.showWarningMessage(
'Unable to revert due to conflicts. Resolve the conflicts before continuing, or abort the revert.',
);
void executeCommand('gitlens.showCommitsView');
void showPausedOperationStatus(this.container, state.repo.path);
}
} catch (ex) {
// Don't show an error message if the user intentionally aborted the revert
Expand All @@ -110,7 +110,7 @@ export class RevertGitCommand extends QuickCommand<State> {
void window.showWarningMessage(
'Unable to revert. A revert is already in progress. Continue or abort the current revert first.',
);
void executeCommand('gitlens.showCommitsView');
void showPausedOperationStatus(this.container, state.repo.path);
return;
}

Expand Down
72 changes: 54 additions & 18 deletions src/git/actions/pausedOperation.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import { window } from 'vscode';
import { PausedOperationAbortError, PausedOperationContinueError } from '@gitlens/git/errors.js';
import type { GitPausedOperationStatus } from '@gitlens/git/models/pausedOperationStatus.js';
import { uncommitted } from '@gitlens/git/models/revision.js';
import { getReferenceLabel } from '@gitlens/git/utils/reference.utils.js';
import type { Source } from '../../constants.telemetry.js';
import type { Container } from '../../container.js';
import { showGitErrorMessage } from '../../messages.js';
import { arePlusFeaturesEnabled } from '../../plus/gk/utils/-webview/plus.utils.js';
import { executeCommand } from '../../system/-webview/command.js';
import type { GitRepositoryService } from '../gitRepositoryService.js';
import { openRebaseEditor } from '../utils/-webview/rebase.utils.js';
Expand All @@ -19,15 +22,19 @@ export async function abortPausedOperation(svc: GitRepositoryService, options?:
}
}

export async function continuePausedOperation(svc: GitRepositoryService): Promise<void> {
return continuePausedOperationCore(svc);
export async function continuePausedOperation(container: Container, svc: GitRepositoryService): Promise<void> {
return continuePausedOperationCore(container, svc);
}

export async function skipPausedOperation(svc: GitRepositoryService): Promise<void> {
return continuePausedOperationCore(svc, true);
export async function skipPausedOperation(container: Container, svc: GitRepositoryService): Promise<void> {
return continuePausedOperationCore(container, svc, true);
}

async function continuePausedOperationCore(svc: GitRepositoryService, skip: boolean = false): Promise<void> {
async function continuePausedOperationCore(
container: Container,
svc: GitRepositoryService,
skip: boolean = false,
): Promise<void> {
try {
return await svc.pausedOps?.continuePausedOperation?.(skip ? { skip: true } : undefined);
} catch (ex) {
Expand All @@ -39,22 +46,22 @@ async function continuePausedOperationCore(svc: GitRepositoryService, skip: bool

const pausedAt = getReferenceLabel(operation.incoming, { icon: false, label: true, quoted: true });

const skip = { title: 'Skip' };
const cancel = { title: 'Cancel', isCloseAffordance: true };
const skipItem = { title: 'Skip' };
const cancelItem = { title: 'Cancel', isCloseAffordance: true };

// TODO@eamodio: We should offer a continue with allowing an empty commit option

const result = await window.showInformationMessage(
`The ${operation.type} operation cannot be continued because ${pausedAt} resulted in an empty commit.\n\nDo you want to skip ${pausedAt}?`,
{ modal: true },
skip,
cancel,
skipItem,
cancelItem,
);
if (result === skip) {
return void continuePausedOperationCore(svc, true);
if (result === skipItem) {
return void continuePausedOperationCore(container, svc, true);
}

void executeCommand('gitlens.showCommitsView');
void showPausedOperationStatus(container, svc.path);

return;
}
Expand All @@ -70,7 +77,7 @@ async function continuePausedOperationCore(svc: GitRepositoryService, skip: bool

if (PausedOperationContinueError.is(ex, 'conflicts') || PausedOperationContinueError.is(ex, 'unmergedFiles')) {
void window.showWarningMessage(ex.message);
void executeCommand('gitlens.showCommitsView');
void showPausedOperationStatus(container, svc.path);
return;
}

Expand All @@ -79,19 +86,48 @@ async function continuePausedOperationCore(svc: GitRepositoryService, skip: bool
}

export interface ShowPausedOperationStatusOptions {
openRebaseEditor?: boolean;
/** When set, the request comes from inside the rebase editor, so always surface the graph */
fromRebaseEditor?: boolean;
source?: Source;
}

/**
* Surfaces a paused operation (merge/rebase/cherry-pick/revert) in a single, consistent place:
* the Graph's WIP details (which renders the paused-op/conflict banner), except for a paused
* rebase when the graph is gated — then the rebase editor, since it's the only usable surface.
*/
export async function showPausedOperationStatus(
container: Container,
repoPath: string,
options?: ShowPausedOperationStatusOptions,
): Promise<void> {
if (options?.openRebaseEditor) {
await openRebaseEditor(container, repoPath);
const svc = container.git.getRepositoryService(repoPath);
// Force a fresh read: a caller may invoke this right after mutating paused-op state (e.g. a
// wizard conflict) before the `'pausedOp'` FS-watcher event lands, so a cached `undefined`
// must not make us silently surface nothing. The readdir-based detection is cheap to repeat.
const status = await svc.pausedOps?.getPausedOperationStatus?.({ force: true });
if (status == null) return;

const toGraph =
options?.fromRebaseEditor || status.type !== 'rebase' || (await isGraphAccessible(container, repoPath));
if (toGraph) {
revealPausedOperationInGraph(repoPath, options?.source);
return;
}

await container.views.commits.show({ preserveFocus: false });
await container.views.commits.revealPausedOperationStatus(repoPath, { focus: true, expand: true, select: true });
await openRebaseEditor(container, repoPath);
}

async function isGraphAccessible(container: Container, repoPath: string): Promise<boolean> {
if (!arePlusFeaturesEnabled()) return false;

return (await container.git.access('graph', repoPath)).allowed !== false;
}

function revealPausedOperationInGraph(repoPath: string, source?: Source): void {
void executeCommand('gitlens.showGraph', {
action: 'show-wip',
target: { sha: uncommitted, worktreePath: repoPath },
source: source,
});
}
18 changes: 0 additions & 18 deletions src/views/commitsView.ts
Original file line number Diff line number Diff line change
Expand Up @@ -439,24 +439,6 @@ export class CommitsView extends ViewBase<'commits', CommitsViewNode, CommitsVie
return node;
}

@gate(() => '')
async revealPausedOperationStatus(repoPath: string, options?: RevealOptions): Promise<ViewNode | undefined> {
const node = await this.findNode(
n => n.is('paused-operation-status') && n.pausedOpStatus.repoPath === repoPath,
{
maxDepth: 3,
canTraverse: n =>
n instanceof CommitsViewNode || n instanceof RepositoryFolderNode || n instanceof BranchNode,
},
);

if (node !== undefined) {
await this.reveal(node, options);
}

return node;
}

private setFilesLayout(layout: ViewFilesLayout) {
return configuration.updateEffective(`views.${this.configKey}.files.layout` as const, layout);
}
Expand Down
10 changes: 8 additions & 2 deletions src/views/viewCommands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -638,15 +638,21 @@ export class ViewCommands implements Disposable {
private async continuePausedOperation(node: PausedOperationStatusNode) {
if (!node.is('paused-operation-status')) return;

await continuePausedOperation(this.container.git.getRepositoryService(node.pausedOpStatus.repoPath));
await continuePausedOperation(
this.container,
this.container.git.getRepositoryService(node.pausedOpStatus.repoPath),
);
}

@command('gitlens.views.pausedOperation.skip')
@debug()
private async skipPausedOperation(node: PausedOperationStatusNode) {
if (!node.is('paused-operation-status')) return;

await skipPausedOperation(this.container.git.getRepositoryService(node.pausedOpStatus.repoPath));
await skipPausedOperation(
this.container,
this.container.git.getRepositoryService(node.pausedOpStatus.repoPath),
);
}

@command('gitlens.views.pausedOperation.open')
Expand Down
8 changes: 3 additions & 5 deletions src/webviews/home/homeWebview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -623,13 +623,13 @@ export class HomeWebviewProvider implements WebviewProvider<State, State, HomeWe
private async continuePausedOperation(pausedOpArgs: GitPausedOperationStatus) {
if (pausedOpArgs.type === 'revert') return;

await continuePausedOperation(this.container.git.getRepositoryService(pausedOpArgs.repoPath));
await continuePausedOperation(this.container, this.container.git.getRepositoryService(pausedOpArgs.repoPath));
}

@command('gitlens.pausedOperation.skip:')
@debug({ args: pausedOpArgs => ({ pausedOpArgs: pausedOpArgs.type }) })
private async skipPausedOperation(pausedOpArgs: GitPausedOperationStatus) {
await skipPausedOperation(this.container.git.getRepositoryService(pausedOpArgs.repoPath));
await skipPausedOperation(this.container, this.container.git.getRepositoryService(pausedOpArgs.repoPath));
}

@command('gitlens.pausedOperation.open:')
Expand All @@ -649,9 +649,7 @@ export class HomeWebviewProvider implements WebviewProvider<State, State, HomeWe
@command('gitlens.pausedOperation.showConflicts:')
@debug({ args: pausedOpArgs => ({ pausedOpArgs: pausedOpArgs.type }) })
private async showConflicts(pausedOpArgs: GitPausedOperationStatus) {
await showPausedOperationStatus(this.container, pausedOpArgs.repoPath, {
openRebaseEditor: pausedOpArgs.type === 'rebase',
});
await showPausedOperationStatus(this.container, pausedOpArgs.repoPath);
}

@command('gitlens.createCloudPatch:')
Expand Down
Loading
Loading