Skip to content

Commit eed0344

Browse files
committed
Impl/stash selected changes
Squashed changes from this PR: desktop#16535
1 parent dce617f commit eed0344

File tree

12 files changed

+385
-4
lines changed

12 files changed

+385
-4
lines changed

app/src/lib/app-state.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,9 @@ export interface IAppState {
218218
/** Whether we should show a confirmation dialog */
219219
readonly askForConfirmationOnDiscardChanges: boolean
220220

221+
/** Whether we should show a confirmation dialog */
222+
readonly askForConfirmationOnStashChanges: boolean
223+
221224
/** Whether we should show a confirmation dialog */
222225
readonly askForConfirmationOnDiscardChangesPermanently: boolean
223226

app/src/lib/error-with-metadata.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,3 +66,19 @@ export class DiscardChangesError extends ErrorWithMetadata {
6666
})
6767
}
6868
}
69+
70+
/**
71+
* An error thrown when a failure occurs while stashing changes.
72+
* Technically just a convenience class on top of ErrorWithMetadata
73+
*/
74+
export class StashChangesError extends ErrorWithMetadata {
75+
public constructor(
76+
error: Error,
77+
repository: Repository,
78+
files: ReadonlyArray<WorkingDirectoryFileChange>
79+
) {
80+
super(error, {
81+
retryAction: { type: RetryActionType.StashChanges, files, repository },
82+
})
83+
}
84+
}

app/src/lib/git/stash.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,7 @@ export async function createDesktopStashEntry(
155155

156156
const branchName = typeof branch === 'string' ? branch : branch.name
157157
const message = createDesktopStashMessage(branchName)
158-
const args = ['stash', 'push', '-m', message]
158+
const args = ['stash', 'push', '-m', message, ...fullySelectedUntrackedFiles.map(x => x.path)]
159159

160160
const result = await git(args, repository.path, 'createStashEntry', {
161161
successExitCodes: new Set<number>([0, 1]),

app/src/lib/stores/app-store.ts

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -289,6 +289,7 @@ import {
289289
ErrorWithMetadata,
290290
CheckoutError,
291291
DiscardChangesError,
292+
StashChangesError,
292293
} from '../error-with-metadata'
293294
import {
294295
ShowSideBySideDiffDefault,
@@ -377,6 +378,7 @@ const confirmRepoRemovalDefault: boolean = true
377378
const showCommitLengthWarningDefault: boolean = false
378379
const confirmDiscardChangesDefault: boolean = true
379380
const confirmDiscardChangesPermanentlyDefault: boolean = true
381+
const confirmStashChangesDefault: boolean = true
380382
const confirmDiscardStashDefault: boolean = true
381383
const confirmCheckoutCommitDefault: boolean = true
382384
const askForConfirmationOnForcePushDefault = true
@@ -385,6 +387,7 @@ const askToMoveToApplicationsFolderKey: string = 'askToMoveToApplicationsFolder'
385387
const confirmRepoRemovalKey: string = 'confirmRepoRemoval'
386388
const showCommitLengthWarningKey: string = 'showCommitLengthWarning'
387389
const confirmDiscardChangesKey: string = 'confirmDiscardChanges'
390+
const confirmStashChangesKey: string = 'confirmStashChanges'
388391
const confirmDiscardStashKey: string = 'confirmDiscardStash'
389392
const confirmCheckoutCommitKey: string = 'confirmCheckoutCommit'
390393
const confirmDiscardChangesPermanentlyKey: string =
@@ -517,6 +520,7 @@ export class AppStore extends TypedBaseStore<IAppState> {
517520
private confirmDiscardChanges: boolean = confirmDiscardChangesDefault
518521
private confirmDiscardChangesPermanently: boolean =
519522
confirmDiscardChangesPermanentlyDefault
523+
private confirmStashChanges: boolean = confirmStashChangesDefault
520524
private confirmDiscardStash: boolean = confirmDiscardStashDefault
521525
private confirmCheckoutCommit: boolean = confirmCheckoutCommitDefault
522526
private askForConfirmationOnForcePush = askForConfirmationOnForcePushDefault
@@ -1044,6 +1048,7 @@ export class AppStore extends TypedBaseStore<IAppState> {
10441048
askForConfirmationOnDiscardChanges: this.confirmDiscardChanges,
10451049
askForConfirmationOnDiscardChangesPermanently:
10461050
this.confirmDiscardChangesPermanently,
1051+
askForConfirmationOnStashChanges: this.confirmStashChanges,
10471052
askForConfirmationOnDiscardStash: this.confirmDiscardStash,
10481053
askForConfirmationOnCheckoutCommit: this.confirmCheckoutCommit,
10491054
askForConfirmationOnForcePush: this.askForConfirmationOnForcePush,
@@ -2200,6 +2205,11 @@ export class AppStore extends TypedBaseStore<IAppState> {
22002205
confirmDiscardChangesPermanentlyDefault
22012206
)
22022207

2208+
this.confirmStashChanges = getBoolean(
2209+
confirmStashChangesKey,
2210+
confirmStashChangesDefault
2211+
)
2212+
22032213
this.confirmDiscardStash = getBoolean(
22042214
confirmDiscardStashKey,
22052215
confirmDiscardStashDefault
@@ -5000,6 +5010,32 @@ export class AppStore extends TypedBaseStore<IAppState> {
50005010
return this._refreshRepository(repository)
50015011
}
50025012

5013+
public async _stashChanges(
5014+
repository: Repository,
5015+
files: ReadonlyArray<WorkingDirectoryFileChange>
5016+
) {
5017+
try {
5018+
const repositoryState = this.repositoryStateCache.get(repository)
5019+
const tip = repositoryState.branchesState.tip
5020+
const currentBranch = tip.kind === TipState.Valid ? tip.branch : null
5021+
5022+
if (currentBranch === null) {
5023+
return
5024+
}
5025+
5026+
await this.createStashEntries(repository, currentBranch, files)
5027+
} catch (error) {
5028+
if (!(error instanceof StashChangesError)) {
5029+
log.error('Failed stashing changes', error)
5030+
}
5031+
5032+
this.emitError(error)
5033+
return
5034+
}
5035+
5036+
return this._refreshRepository(repository)
5037+
}
5038+
50035039
public async _discardChangesFromSelection(
50045040
repository: Repository,
50055041
filePath: string,
@@ -5757,6 +5793,15 @@ export class AppStore extends TypedBaseStore<IAppState> {
57575793
return Promise.resolve()
57585794
}
57595795

5796+
public _setConfirmStashChangesSetting(value: boolean): Promise<void> {
5797+
this.confirmStashChanges = value
5798+
5799+
setBoolean(confirmStashChangesKey, value)
5800+
this.emitUpdate()
5801+
5802+
return Promise.resolve()
5803+
}
5804+
57605805
public _setConfirmDiscardChangesPermanentlySetting(
57615806
value: boolean
57625807
): Promise<void> {
@@ -6875,13 +6920,36 @@ export class AppStore extends TypedBaseStore<IAppState> {
68756920
}
68766921

68776922
private async createStashEntry(repository: Repository, branch: Branch) {
6923+
const stashEntry = await getLastDesktopStashEntryForBranch(repository, branch)
6924+
6925+
if (stashEntry !== null) {
6926+
await this._popStashEntry(repository, stashEntry)
6927+
}
6928+
68786929
const { changesState } = this.repositoryStateCache.get(repository)
68796930
const { workingDirectory } = changesState
68806931
const untrackedFiles = getUntrackedFiles(workingDirectory)
68816932

68826933
return createDesktopStashEntry(repository, branch, untrackedFiles)
68836934
}
68846935

6936+
private async createStashEntries(repository: Repository, branch: Branch, files: ReadonlyArray<WorkingDirectoryFileChange>) {
6937+
const { changesState } = this.repositoryStateCache.get(repository)
6938+
const { workingDirectory } = changesState
6939+
6940+
const stashEntry = await getLastDesktopStashEntryForBranch(repository, branch)
6941+
6942+
if (stashEntry !== null) {
6943+
await this._popStashEntry(repository, stashEntry)
6944+
}
6945+
6946+
const newChangesState = this.repositoryStateCache.get(repository).changesState
6947+
const newWorkingDirectory = newChangesState.workingDirectory
6948+
const stashPoppedFiles = newWorkingDirectory.files.filter(stashFile => workingDirectory.files.findIndex(file => file.path === stashFile.path) === -1)
6949+
6950+
return createDesktopStashEntry(repository, branch, [...files, ...stashPoppedFiles])
6951+
}
6952+
68856953
/** This shouldn't be called directly. See `Dispatcher`. */
68866954
public async _popStashEntry(repository: Repository, stashEntry: IStashEntry) {
68876955
await popStashEntry(repository, stashEntry.stashSha)

app/src/models/popup.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ export enum PopupType {
2929
DeleteBranch = 'DeleteBranch',
3030
DeleteRemoteBranch = 'DeleteRemoteBranch',
3131
ConfirmDiscardChanges = 'ConfirmDiscardChanges',
32+
ConfirmStashChanges = 'ConfirmStashChanges',
3233
Preferences = 'Preferences',
3334
RepositorySettings = 'RepositorySettings',
3435
AddRepository = 'AddRepository',
@@ -126,6 +127,13 @@ export type PopupDetail =
126127
discardingAllChanges?: boolean
127128
permanentlyDelete?: boolean
128129
}
130+
| {
131+
type: PopupType.ConfirmStashChanges
132+
repository: Repository
133+
files: ReadonlyArray<WorkingDirectoryFileChange>
134+
showStashChangesSetting?: boolean
135+
stashingAllChanges?: boolean
136+
}
129137
| {
130138
type: PopupType.ConfirmDiscardSelection
131139
repository: Repository

app/src/models/retry-actions.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ export enum RetryActionType {
1818
Squash,
1919
Reorder,
2020
DiscardChanges,
21+
StashChanges,
2122
}
2223

2324
/** The retriable actions and their associated data. */
@@ -85,3 +86,8 @@ export type RetryAction =
8586
repository: Repository
8687
files: ReadonlyArray<WorkingDirectoryFileChange>
8788
}
89+
| {
90+
type: RetryActionType.StashChanges
91+
repository: Repository
92+
files: ReadonlyArray<WorkingDirectoryFileChange>
93+
}

app/src/ui/app.tsx

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,7 @@ import { TestNotifications } from './test-notifications/test-notifications'
177177
import { NotificationsDebugStore } from '../lib/stores/notifications-debug-store'
178178
import { PullRequestComment } from './notifications/pull-request-comment'
179179
import { UnknownAuthors } from './unknown-authors/unknown-authors-dialog'
180+
import { StashChanges } from './stash-changes/stash-changes-dialog'
180181
import { UnsupportedOSBannerDismissedAtKey } from './banners/os-version-no-longer-supported-banner'
181182
import { offsetFromNow } from '../lib/offset-from'
182183
import { getBoolean, getNumber } from '../lib/local-storage'
@@ -1700,6 +1701,29 @@ export class App extends React.Component<IAppProps, IAppState> {
17001701
onConfirmDiscardChangesChanged={this.onConfirmDiscardChangesChanged}
17011702
/>
17021703
)
1704+
case PopupType.ConfirmStashChanges:
1705+
const showStashChangesSetting =
1706+
popup.showStashChangesSetting === undefined
1707+
? true
1708+
: popup.showStashChangesSetting
1709+
const stashingAllChanges =
1710+
popup.stashingAllChanges === undefined
1711+
? false
1712+
: popup.stashingAllChanges
1713+
1714+
return (
1715+
<StashChanges
1716+
key="stash-changes"
1717+
repository={popup.repository}
1718+
dispatcher={this.props.dispatcher}
1719+
files={popup.files}
1720+
confirmStashChanges={this.state.askForConfirmationOnStashChanges}
1721+
showStashChangesSetting={showStashChangesSetting}
1722+
stashingAllChanges={stashingAllChanges}
1723+
onDismissed={onPopupDismissedFn}
1724+
onConfirmStashChangesChanged={this.onConfirmStashChangesChanged}
1725+
/>
1726+
)
17031727
case PopupType.ConfirmDiscardSelection:
17041728
return (
17051729
<DiscardSelection
@@ -2824,6 +2848,10 @@ export class App extends React.Component<IAppProps, IAppState> {
28242848
this.props.dispatcher.setConfirmDiscardChangesSetting(value)
28252849
}
28262850

2851+
private onConfirmStashChangesChanged = (value: boolean) => {
2852+
this.props.dispatcher.setConfirmStashChangesSetting(value)
2853+
}
2854+
28272855
private onConfirmDiscardChangesPermanentlyChanged = (value: boolean) => {
28282856
this.props.dispatcher.setConfirmDiscardChangesPermanentlySetting(value)
28292857
}

app/src/ui/changes/changes-list.tsx

Lines changed: 51 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,10 @@ interface IChangesListProps {
128128
isDiscardingAllChanges: boolean,
129129
permanently: boolean
130130
) => void
131+
readonly onStashChangesFromFiles: (
132+
files: ReadonlyArray<WorkingDirectoryFileChange>,
133+
isStashingAllChanges: boolean
134+
) => void
131135

132136
/** Callback that fires on page scroll to pass the new scrollTop location */
133137
readonly onChangesListScrolled: (scrollTop: number) => void
@@ -328,6 +332,10 @@ export class ChangesList extends React.Component<
328332
)
329333
}
330334

335+
private onStashAllChanges = () => {
336+
this.props.dispatcher.createStashForCurrentBranch(this.props.repository)
337+
}
338+
331339
private onDiscardAllChanges = () => {
332340
this.props.onDiscardChangesFromFiles(
333341
this.props.workingDirectory.files,
@@ -344,8 +352,25 @@ export class ChangesList extends React.Component<
344352
)
345353
}
346354

347-
private onStashChanges = () => {
348-
this.props.dispatcher.createStashForCurrentBranch(this.props.repository)
355+
private onStashChanges = (files: ReadonlyArray<string>) => {
356+
const workingDirectory = this.props.workingDirectory
357+
const modifiedFiles = new Array<WorkingDirectoryFileChange>()
358+
359+
files.forEach(file => {
360+
const modifiedFile = workingDirectory.files.find(f => f.path === file)
361+
362+
if (modifiedFile != null) {
363+
modifiedFiles.push(modifiedFile)
364+
}
365+
})
366+
367+
const stashingAllChanges =
368+
modifiedFiles.length === workingDirectory.files.length
369+
370+
this.props.onStashChangesFromFiles(
371+
modifiedFiles,
372+
stashingAllChanges
373+
)
349374
}
350375

351376
private onDiscardChanges = (files: ReadonlyArray<string>) => {
@@ -396,6 +421,19 @@ export class ChangesList extends React.Component<
396421
return this.props.askForConfirmationOnDiscardChanges ? `${label}…` : label
397422
}
398423

424+
private getStashChangesMenuItemLabel = (files: ReadonlyArray<string>) => {
425+
const label =
426+
files.length === 1
427+
? __DARWIN__
428+
? `Stash Changes`
429+
: `Stash changes`
430+
: __DARWIN__
431+
? `Stash ${files.length} Selected Changes`
432+
: `Stash ${files.length} selected changes`
433+
434+
return this.props.askForConfirmationOnDiscardChanges ? `${label}…` : label
435+
}
436+
399437
private onContextMenu = (event: React.MouseEvent<any>) => {
400438
event.preventDefault()
401439

@@ -432,7 +470,7 @@ export class ChangesList extends React.Component<
432470
},
433471
{
434472
label: hasStash ? confirmStashAllChangesLabel : stashAllChangesLabel,
435-
action: this.onStashChanges,
473+
action: this.onStashAllChanges,
436474
enabled: hasLocalChanges && this.props.branch !== null && !hasConflicts,
437475
},
438476
]
@@ -449,6 +487,15 @@ export class ChangesList extends React.Component<
449487
}
450488
}
451489

490+
private getStashChangesMenuItem = (
491+
paths: ReadonlyArray<string>
492+
): IMenuItem => {
493+
return {
494+
label: this.getStashChangesMenuItemLabel(paths),
495+
action: () => this.onStashChanges(paths),
496+
}
497+
}
498+
452499
private getCopyPathMenuItem = (
453500
file: WorkingDirectoryFileChange
454501
): IMenuItem => {
@@ -564,6 +611,7 @@ export class ChangesList extends React.Component<
564611

565612
const items: IMenuItem[] = [
566613
this.getDiscardChangesMenuItem(paths),
614+
this.getStashChangesMenuItem(paths),
567615
{ type: 'separator' },
568616
]
569617
if (paths.length === 1) {

app/src/ui/changes/sidebar.tsx

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,19 @@ export class ChangesSidebar extends React.Component<IChangesSidebarProps, {}> {
230230
}
231231
}
232232

233+
private onStashChangesFromFiles = (
234+
files: ReadonlyArray<WorkingDirectoryFileChange>,
235+
isStashingAllChanges: boolean
236+
) => {
237+
this.props.dispatcher.showPopup({
238+
type: PopupType.ConfirmStashChanges,
239+
repository: this.props.repository,
240+
showStashChangesSetting: false,
241+
stashingAllChanges: isStashingAllChanges,
242+
files,
243+
})
244+
}
245+
233246
private onDiscardChangesFromFiles = (
234247
files: ReadonlyArray<WorkingDirectoryFileChange>,
235248
isDiscardingAllChanges: boolean,
@@ -418,6 +431,7 @@ export class ChangesSidebar extends React.Component<IChangesSidebarProps, {}> {
418431
this.props.askForConfirmationOnDiscardChanges
419432
}
420433
onDiscardChangesFromFiles={this.onDiscardChangesFromFiles}
434+
onStashChangesFromFiles={this.onStashChangesFromFiles}
421435
onOpenItem={this.onOpenItem}
422436
onRowClick={this.onChangedItemClick}
423437
commitAuthor={this.props.commitAuthor}

0 commit comments

Comments
 (0)