Skip to content

Commit

Permalink
Fixes stashing when staged is not supported
Browse files Browse the repository at this point in the history
Co-authored-by: Eric Amodio <eamodio@users.noreply.github.com>
  • Loading branch information
d13 and eamodio committed Apr 5, 2023
1 parent fdaca49 commit 5f949a9
Show file tree
Hide file tree
Showing 9 changed files with 137 additions and 25 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Expand Up @@ -32,6 +32,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p
- Fixes [#2582](https://github.com/gitkraken/vscode-gitlens/issues/2582) - _Visual File History_ background color when in a panel
- Fixes [#2609](https://github.com/gitkraken/vscode-gitlens/issues/2609) - If you check out a branch that is hidden, GitLens should show the branch still
- Fixes tooltips sometimes failing to show in _Commit Graph_ rows when the Date column is hidden
- Fixes [#2595](https://github.com/gitkraken/vscode-gitlens/issues/2595) - Error when stashing changes

## [13.4.0] - 2023-03-16

Expand Down
31 changes: 29 additions & 2 deletions src/commands/git/stash.ts
Expand Up @@ -4,7 +4,7 @@ import { GlyphChars } from '../../constants';
import type { Container } from '../../container';
import { getContext } from '../../context';
import { reveal, showDetailsView } from '../../git/actions/stash';
import { StashApplyError, StashApplyErrorReason } from '../../git/errors';
import { StashApplyError, StashApplyErrorReason, StashPushError, StashPushErrorReason } from '../../git/errors';
import type { GitStashCommit } from '../../git/models/commit';
import type { GitStashReference } from '../../git/models/reference';
import { getReferenceLabel } from '../../git/models/reference';
Expand Down Expand Up @@ -82,6 +82,7 @@ interface PushState {
repo: string | Repository;
message?: string;
uris?: Uri[];
onlyStagedUris?: Uri[];
flags: PushFlags[];
}

Expand Down Expand Up @@ -514,16 +515,42 @@ export class StashGitCommand extends QuickCommand<State> {
state.flags = result;
}

endSteps(state);
try {
await state.repo.stashSave(state.message, state.uris, {
includeUntracked: state.flags.includes('--include-untracked'),
keepIndex: state.flags.includes('--keep-index'),
onlyStaged: state.flags.includes('--staged'),
});

endSteps(state);
} catch (ex) {
Logger.error(ex, context.title);

if (
ex instanceof StashPushError &&
ex.reason === StashPushErrorReason.ConflictingStagedAndUnstagedLines &&
state.flags.includes('--staged')
) {
const confirm = { title: 'Yes' };
const cancel = { title: 'No', isCloseAffordance: true };
const result = await window.showErrorMessage(
ex.message,
{
modal: true,
},
confirm,
cancel,
);

if (result === confirm) {
state.uris = state.onlyStagedUris;
state.flags.splice(state.flags.indexOf('--staged'), 1);
continue;
}

return;
}

const msg: string = ex?.message ?? ex?.toString() ?? '';
if (msg.includes('newer version of Git')) {
void window.showErrorMessage(`Unable to stash changes. ${msg}`);
Expand Down
62 changes: 41 additions & 21 deletions src/commands/stashSave.ts
Expand Up @@ -3,6 +3,7 @@ import type { ScmResource } from '../@types/vscode.git.resources';
import { ScmResourceGroupType } from '../@types/vscode.git.resources.enums';
import { Commands } from '../constants';
import type { Container } from '../container';
import { Features } from '../features';
import { push } from '../git/actions/stash';
import { GitUri } from '../git/gitUri';
import { command } from '../system/command';
Expand All @@ -20,6 +21,7 @@ export interface StashSaveCommandArgs {
uris?: Uri[];
keepStaged?: boolean;
onlyStaged?: boolean;
onlyStagedUris?: Uri[];
}

@command()
Expand All @@ -44,33 +46,44 @@ export class StashSaveCommand extends Command {
args.uris = context.scmResourceStates.map(s => s.resourceUri);
args.repoPath = (await this.container.git.getOrOpenRepository(args.uris[0]))?.path;

const status = await this.container.git.getStatusForRepo(args.repoPath);
if (status?.computeWorkingTreeStatus().staged) {
if (
!context.scmResourceStates.some(
s => (s as ScmResource).resourceGroupType === ScmResourceGroupType.Index,
)
) {
args.keepStaged = true;
}
if (
!context.scmResourceStates.some(
s => (s as ScmResource).resourceGroupType === ScmResourceGroupType.Index,
)
) {
args.keepStaged = true;
}
} else if (context.type === 'scm-groups') {
args = { ...args };

if (context.scmResourceGroups.every(g => g.id === 'index')) {
let isStagedOnly = true;
let hasStaged = false;
const uris = context.scmResourceGroups.reduce<Uri[]>((a, b) => {
const isStaged = b.id === 'index';
if (isStagedOnly && !isStaged) {
isStagedOnly = false;
}
if (isStaged) {
hasStaged = true;
}
return a.concat(b.resourceStates.map(s => s.resourceUri));
}, []);

const repo = await this.container.git.getOrOpenRepository(uris[0]);
let canUseStagedOnly = false;
if (isStagedOnly && repo != null) {
canUseStagedOnly = await repo.supports(Features.StashOnlyStaged);
}

if (canUseStagedOnly) {
args.onlyStaged = true;
args.onlyStagedUris = uris;
} else {
args.uris = context.scmResourceGroups.reduce<Uri[]>(
(a, b) => a.concat(b.resourceStates.map(s => s.resourceUri)),
[],
);
args.repoPath = (await this.container.git.getOrOpenRepository(args.uris[0]))?.path;
args.uris = uris;
args.repoPath = repo?.path;

const status = await this.container.git.getStatusForRepo(args.repoPath);
if (status?.computeWorkingTreeStatus().staged) {
if (!context.scmResourceGroups.some(g => g.id === 'index')) {
args.keepStaged = true;
}
if (!hasStaged) {
args.keepStaged = true;
}
}
}
Expand All @@ -79,6 +92,13 @@ export class StashSaveCommand extends Command {
}

execute(args?: StashSaveCommandArgs) {
return push(args?.repoPath, args?.uris, args?.message, args?.keepStaged, args?.onlyStaged);
return push(
args?.repoPath,
args?.uris,
args?.message,
args?.keepStaged,
args?.onlyStaged,
args?.onlyStagedUris,
);
}
}
20 changes: 18 additions & 2 deletions src/env/node/git/git.ts
Expand Up @@ -8,6 +8,7 @@ import { hrtime } from '@env/hrtime';
import { GlyphChars } from '../../../constants';
import type { GitCommandOptions, GitSpawnOptions } from '../../../git/commandOptions';
import { GitErrorHandling } from '../../../git/commandOptions';
import { StashPushError, StashPushErrorReason } from '../../../git/errors';
import type { GitDiffFilter } from '../../../git/models/diff';
import { isUncommitted, isUncommittedStaged, shortenRevision } from '../../../git/models/reference';
import type { GitUser } from '../../../git/models/user';
Expand Down Expand Up @@ -1859,7 +1860,11 @@ export class Git {
}

if (onlyStaged) {
params.push('--staged');
if (await this.isAtLeastVersion('2.35')) {
params.push('--staged');
} else {
throw new Error('Git version 2.35 or higher is required for --staged');
}
}

if (message) {
Expand All @@ -1882,7 +1887,18 @@ export class Git {
params.push(...pathspecs);
}

void (await this.git<string>({ cwd: repoPath }, ...params));
try {
void (await this.git<string>({ cwd: repoPath }, ...params));
} catch (ex) {
if (
ex instanceof RunError &&
ex.stdout.includes('Saved working directory and index state') &&
ex.stderr.includes('Cannot remove worktree changes')
) {
throw new StashPushError(StashPushErrorReason.ConflictingStagedAndUnstagedLines);
}
throw ex;
}
}

async status(
Expand Down
4 changes: 4 additions & 0 deletions src/env/node/git/localGitProvider.ts
Expand Up @@ -502,6 +502,10 @@ export class LocalGitProvider implements GitProvider, Disposable {
supported = await this.git.isAtLeastVersion('2.17.0');
this._supportedFeatures.set(feature, supported);
return supported;
case Features.StashOnlyStaged:
supported = await this.git.isAtLeastVersion('2.35.0');
this._supportedFeatures.set(feature, supported);
return supported;
default:
return true;
}
Expand Down
1 change: 1 addition & 0 deletions src/features.ts
Expand Up @@ -5,6 +5,7 @@ export const enum Features {
Stashes = 'stashes',
Timeline = 'timeline',
Worktrees = 'worktrees',
StashOnlyStaged = 'stashOnlyStaged',
}

export type FeatureAccess =
Expand Down
2 changes: 2 additions & 0 deletions src/git/actions/stash.ts
Expand Up @@ -33,13 +33,15 @@ export function push(
message?: string,
keepStaged: boolean = false,
onlyStaged: boolean = false,
onlyStagedUris?: Uri[],
) {
return executeGitCommand({
command: 'stash',
state: {
subcommand: 'push',
repo: repo,
uris: uris,
onlyStagedUris: onlyStagedUris,
message: message,
flags: [...(keepStaged ? ['--keep-index'] : []), ...(onlyStaged ? ['--staged'] : [])] as PushFlags[],
},
Expand Down
40 changes: 40 additions & 0 deletions src/git/errors.ts
Expand Up @@ -41,6 +41,46 @@ export class StashApplyError extends Error {
}
}

export const enum StashPushErrorReason {
ConflictingStagedAndUnstagedLines = 1,
}

export class StashPushError extends Error {
static is(ex: any, reason?: StashPushErrorReason): ex is StashPushError {
return ex instanceof StashPushError && (reason == null || ex.reason === reason);
}

readonly original?: Error;
readonly reason: StashPushErrorReason | undefined;

constructor(reason?: StashPushErrorReason, original?: Error);
constructor(message?: string, original?: Error);
constructor(messageOrReason: string | StashPushErrorReason | undefined, original?: Error) {
let message;
let reason: StashPushErrorReason | undefined;
if (messageOrReason == null) {
message = 'Unable to stash';
} else if (typeof messageOrReason === 'string') {
message = messageOrReason;
reason = undefined;
} else {
reason = messageOrReason;
switch (reason) {
case StashPushErrorReason.ConflictingStagedAndUnstagedLines:
message =
'Stash was created, but the working tree cannot be updated because at least one file has staged and unstaged changes on the same line(s).\n\nDo you want to try again by stashing both your staged and unstaged changes?';
break;
default:
message = 'Unable to stash';
}
}
super(message);

this.original = original;
this.reason = reason;
Error.captureStackTrace?.(this, StashApplyError);
}
}
export const enum WorktreeCreateErrorReason {
AlreadyCheckedOut = 1,
AlreadyExists = 2,
Expand Down
1 change: 1 addition & 0 deletions src/plus/github/githubGitProvider.ts
Expand Up @@ -226,6 +226,7 @@ export class GitHubGitProvider implements GitProvider, Disposable {
switch (feature) {
case Features.Stashes:
case Features.Worktrees:
case Features.StashOnlyStaged:
return false;
default:
return true;
Expand Down

0 comments on commit 5f949a9

Please sign in to comment.