Skip to content

Commit 897a7e1

Browse files
authored
Better handle case of unsaved changes (#1115)
* Better handle case of unsaved changes Fixes #853 * Fix Jest tests
1 parent f6ef126 commit 897a7e1

File tree

6 files changed

+81
-72
lines changed

6 files changed

+81
-72
lines changed

src/components/CommitBox.tsx

Lines changed: 4 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,6 @@ import {
2828
} from '../style/NewBranchDialog';
2929
import { CommandIDs } from '../tokens';
3030
import { CommitMessage } from './CommitMessage';
31-
import { WarningBox } from './WarningBox';
32-
import { WarningRounded as WarningRoundedIcon } from '@material-ui/icons';
3331

3432
/**
3533
* Commit action
@@ -85,9 +83,9 @@ export interface ICommitBoxProps {
8583
amend: boolean;
8684

8785
/**
88-
* Whether the warning box for dirty (e.g., unsaved) staged files is shown.
86+
* Warning element to display
8987
*/
90-
warnDirtyStagedFiles: boolean;
88+
warning?: JSX.Element | null;
9189

9290
/**
9391
* Updates the commit message summary.
@@ -192,19 +190,10 @@ export class CommitBox extends React.Component<
192190
'Summary (%1 to commit)',
193191
shortcutHint
194192
);
195-
const warningTitle = this.props.trans.__('Warning');
196-
const warningContent = this.props.trans.__(
197-
'You have unsaved staged files. You probably want to save and stage all needed changes before committing.'
198-
);
193+
199194
return (
200195
<div className={classes(commitFormClass, 'jp-git-CommitBox')}>
201-
{this.props.warnDirtyStagedFiles && (
202-
<WarningBox
203-
headerIcon={<WarningRoundedIcon />}
204-
title={warningTitle}
205-
content={warningContent}
206-
/>
207-
)}
196+
{this.props.warning ?? null}
208197
{!this.props.amend && (
209198
<CommitMessage
210199
error={

src/components/GitPanel.tsx

Lines changed: 33 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ import { CommitComparisonBox } from './CommitComparisonBox';
2828
import { FileList } from './FileList';
2929
import { HistorySideBar } from './HistorySideBar';
3030
import { Toolbar } from './Toolbar';
31+
import { WarningBox } from './WarningBox';
32+
import { WarningRounded as WarningRoundedIcon } from '@material-ui/icons';
3133

3234
/**
3335
* Interface describing component properties.
@@ -131,7 +133,7 @@ export interface IGitPanelState {
131133
/**
132134
* Whether there are dirty (e.g., unsaved) staged files.
133135
*/
134-
hasDirtyStagedFiles: boolean;
136+
hasDirtyFiles: boolean;
135137

136138
/**
137139
* The commit to compare against
@@ -156,8 +158,12 @@ export class GitPanel extends React.Component<IGitPanelProps, IGitPanelState> {
156158
*/
157159
constructor(props: IGitPanelProps) {
158160
super(props);
159-
const { branches, currentBranch, pathRepository, hasDirtyStagedFiles } =
160-
props.model;
161+
const {
162+
branches,
163+
currentBranch,
164+
pathRepository,
165+
hasDirtyFiles: hasDirtyStagedFiles
166+
} = props.model;
161167

162168
this.state = {
163169
branches: branches,
@@ -172,7 +178,7 @@ export class GitPanel extends React.Component<IGitPanelProps, IGitPanelState> {
172178
commitSummary: '',
173179
commitDescription: '',
174180
commitAmend: false,
175-
hasDirtyStagedFiles: hasDirtyStagedFiles,
181+
hasDirtyFiles: hasDirtyStagedFiles,
176182
referenceCommit: null,
177183
challengerCommit: null
178184
};
@@ -222,9 +228,9 @@ export class GitPanel extends React.Component<IGitPanelProps, IGitPanelState> {
222228

223229
settings.changed.connect(this.refreshView, this);
224230

225-
model.dirtyStagedFilesStatusChanged.connect((_, args) => {
231+
model.dirtyFilesStatusChanged.connect((_, args) => {
226232
this.setState({
227-
hasDirtyStagedFiles: args
233+
hasDirtyFiles: args
228234
});
229235
});
230236
}
@@ -432,6 +438,17 @@ export class GitPanel extends React.Component<IGitPanelProps, IGitPanelState> {
432438
: this.state.commitAmend
433439
? this.props.trans.__('Commit (Amend)')
434440
: this.props.trans.__('Commit');
441+
const warningTitle = this.props.trans.__('Warning');
442+
const inSimpleMode = this.props.settings.composite[
443+
'simpleStaging'
444+
] as boolean;
445+
const warningContent = inSimpleMode
446+
? this.props.trans.__(
447+
'You have unsaved tracked files. You probably want to save all changes before committing.'
448+
)
449+
: this.props.trans.__(
450+
'You have unsaved staged files. You probably want to save and stage all needed changes before committing.'
451+
);
435452
return (
436453
<React.Fragment>
437454
<FileList
@@ -444,9 +461,7 @@ export class GitPanel extends React.Component<IGitPanelProps, IGitPanelState> {
444461
<CommitBox
445462
commands={this.props.commands}
446463
hasFiles={
447-
this.props.settings.composite['simpleStaging']
448-
? this._markedFiles.length > 0
449-
: this._hasStagedFile()
464+
inSimpleMode ? this._markedFiles.length > 0 : this._hasStagedFile()
450465
}
451466
trans={this.props.trans}
452467
label={buttonLabel}
@@ -457,7 +472,15 @@ export class GitPanel extends React.Component<IGitPanelProps, IGitPanelState> {
457472
setDescription={this._setCommitDescription}
458473
setAmend={this._setCommitAmend}
459474
onCommit={this.commitFiles}
460-
warnDirtyStagedFiles={this.state.hasDirtyStagedFiles}
475+
warning={
476+
this.state.hasDirtyFiles && (
477+
<WarningBox
478+
headerIcon={<WarningRoundedIcon />}
479+
title={warningTitle}
480+
content={warningContent}
481+
/>
482+
)
483+
}
461484
/>
462485
</React.Fragment>
463486
);

src/model.ts

Lines changed: 38 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -53,17 +53,11 @@ export class GitExtension implements IGitExtension {
5353
// Initialize repository status
5454
this._clearStatus();
5555

56-
let interval: number;
57-
if (settings) {
58-
interval = settings.composite.refreshInterval as number;
59-
settings.changed.connect(this._onSettingsChange, this);
60-
} else {
61-
interval = DEFAULT_REFRESH_INTERVAL;
62-
}
56+
const interval = DEFAULT_REFRESH_INTERVAL;
6357
this._statusPoll = new Poll({
6458
factory: this._refreshModel,
6559
frequency: {
66-
interval: interval,
60+
interval,
6761
backoff: true,
6862
max: 300 * 1000
6963
},
@@ -79,6 +73,11 @@ export class GitExtension implements IGitExtension {
7973
},
8074
standby: this._refreshStandby
8175
});
76+
77+
if (settings) {
78+
settings.changed.connect(this._onSettingsChange, this);
79+
this._onSettingsChange(settings);
80+
}
8281
}
8382

8483
/**
@@ -269,23 +268,17 @@ export class GitExtension implements IGitExtension {
269268
}
270269

271270
/**
272-
* Boolean indicating whether there are dirty staged files
273-
* (e.g., due to unsaved changes on files that have been previously staged).
271+
* Boolean indicating whether there are dirty files
272+
* A dirty file is a file with unsaved changes that is staged in classical mode
273+
* or modified in simple mode.
274274
*/
275-
get hasDirtyStagedFiles(): boolean {
276-
return this._hasDirtyStagedFiles;
275+
get hasDirtyFiles(): boolean {
276+
return this._hasDirtyFiles;
277277
}
278-
/**
279-
* Updates the value of the boolean indicating whether there are dirty staged files
280-
* (e.g., due to unsaved changes on files that have been previously staged).
281-
* Emits a signal indicating if necessary.
282-
* This signal is emitted when there is a dirty staged file but none previously,
283-
* and vice versa, when there are no dirty staged files but there were some previously.
284-
*/
285-
set hasDirtyStagedFiles(value: boolean) {
286-
if (this._hasDirtyStagedFiles !== value) {
287-
this._hasDirtyStagedFiles = value;
288-
this._dirtyStagedFilesStatusChanged.emit(value);
278+
set hasDirtyFiles(value: boolean) {
279+
if (this._hasDirtyFiles !== value) {
280+
this._hasDirtyFiles = value;
281+
this._dirtyFilesStatusChanged.emit(value);
289282
}
290283
}
291284

@@ -294,8 +287,8 @@ export class GitExtension implements IGitExtension {
294287
* This signal is emitted when there is a dirty staged file but none previously,
295288
* and vice versa, when there are no dirty staged files but there were some previously.
296289
*/
297-
get dirtyStagedFilesStatusChanged(): ISignal<IGitExtension, boolean> {
298-
return this._dirtyStagedFilesStatusChanged;
290+
get dirtyFilesStatusChanged(): ISignal<IGitExtension, boolean> {
291+
return this._dirtyFilesStatusChanged;
299292
}
300293

301294
/**
@@ -1090,7 +1083,7 @@ export class GitExtension implements IGitExtension {
10901083
behind: data.behind || 0,
10911084
files
10921085
});
1093-
await this.refreshDirtyStagedStatus();
1086+
await this.refreshDirtyStatus();
10941087
} catch (err) {
10951088
// TODO we should notify the user
10961089
this._clearStatus();
@@ -1179,19 +1172,18 @@ export class GitExtension implements IGitExtension {
11791172
}
11801173

11811174
/**
1182-
* Determines whether there are unsaved changes on staged files,
1183-
* e.g., the user has made changes to a file that has been staged,
1184-
* but has not saved them.
1185-
* @returns promise that resolves upon refreshing the dirty status of staged files
1175+
* Determines whether there are unsaved changes on files,
1176+
*
1177+
* @returns promise that resolves upon refreshing the dirty status of files
11861178
*/
1187-
async refreshDirtyStagedStatus(): Promise<void> {
1179+
async refreshDirtyStatus(): Promise<void> {
11881180
// we assume the repository status has been refreshed prior to this
11891181

1190-
// get staged files
1191-
const stagedFiles = this.status.files.filter(
1192-
file => file.status === 'staged' || file.status === 'partially-staged'
1182+
// get files
1183+
const files = this.status.files.filter(file =>
1184+
this._statusForDirtyState.includes(file.status)
11931185
);
1194-
const fileNames = stagedFiles.map(file => file.to);
1186+
const fileNames = files.map(file => file.to);
11951187

11961188
let result = false;
11971189

@@ -1208,7 +1200,7 @@ export class GitExtension implements IGitExtension {
12081200
}
12091201
}
12101202

1211-
this.hasDirtyStagedFiles = result;
1203+
this.hasDirtyFiles = result;
12121204
}
12131205

12141206
/**
@@ -1631,6 +1623,11 @@ export class GitExtension implements IGitExtension {
16311623
...this._statusPoll.frequency,
16321624
interval: settings.composite.refreshInterval as number
16331625
};
1626+
1627+
this._statusForDirtyState = (settings.composite.simpleStaging as boolean)
1628+
? ['staged', 'partially-staged', 'unstaged']
1629+
: ['staged', 'partially-staged'];
1630+
this.refreshDirtyStatus();
16341631
}
16351632

16361633
/**
@@ -1721,9 +1718,12 @@ export class GitExtension implements IGitExtension {
17211718
private _remoteChangedFiles: Git.IStatusFile[] = [];
17221719
private _changeUpstreamNotified: Git.IStatusFile[] = [];
17231720
private _selectedHistoryFile: Git.IStatusFile | null = null;
1724-
private _hasDirtyStagedFiles = false;
1721+
private _hasDirtyFiles = false;
17251722
private _credentialsRequired = false;
17261723

1724+
// Configurable
1725+
private _statusForDirtyState: Git.Status[] = ['staged', 'partially-staged'];
1726+
17271727
private _branchesChanged = new Signal<IGitExtension, void>(this);
17281728
private _headChanged = new Signal<IGitExtension, void>(this);
17291729
private _markChanged = new Signal<IGitExtension, void>(this);
@@ -1740,9 +1740,7 @@ export class GitExtension implements IGitExtension {
17401740
IGitExtension,
17411741
Git.IRemoteChangedNotification | null
17421742
>(this);
1743-
private _dirtyStagedFilesStatusChanged = new Signal<IGitExtension, boolean>(
1744-
this
1745-
);
1743+
private _dirtyFilesStatusChanged = new Signal<IGitExtension, boolean>(this);
17461744
private _credentialsRequiredChanged = new Signal<IGitExtension, boolean>(
17471745
this
17481746
);

src/tokens.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ export interface IGitExtension extends IDisposable {
7070
* Boolean indicating whether there are dirty staged files
7171
* (e.g., due to unsaved changes on files that have been previously staged).
7272
*/
73-
hasDirtyStagedFiles: boolean;
73+
hasDirtyFiles: boolean;
7474

7575
/**
7676
* Boolean indicating whether credentials are required from the user.
@@ -118,7 +118,7 @@ export interface IGitExtension extends IDisposable {
118118
* This signal is emitted when there is a dirty staged file but none in prior,
119119
* and vice versa, when there are no dirty staged files but there were previously.
120120
*/
121-
readonly dirtyStagedFilesStatusChanged: ISignal<IGitExtension, boolean>;
121+
readonly dirtyFilesStatusChanged: ISignal<IGitExtension, boolean>;
122122

123123
/**
124124
* Add one or more files to the repository staging area.
@@ -445,7 +445,7 @@ export interface IGitExtension extends IDisposable {
445445
* Emits a signal indicating if there are unsaved changes.
446446
* @returns promise that resolves upon refreshing the dirty status of staged files
447447
*/
448-
refreshDirtyStagedStatus(): Promise<void>;
448+
refreshDirtyStatus(): Promise<void>;
449449

450450
/**
451451
* Request Git status refresh

tests/test-components/CommitBox.spec.tsx

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,7 @@ describe('CommitBox', () => {
3030
hasFiles: false,
3131
commands: defaultCommands,
3232
trans: trans,
33-
label: 'Commit',
34-
warnDirtyStagedFiles: false
33+
label: 'Commit'
3534
};
3635

3736
describe('#constructor()', () => {
@@ -139,7 +138,7 @@ describe('CommitBox', () => {
139138
it('should render a warning box when there are dirty staged files', () => {
140139
const props = {
141140
...defaultProps,
142-
warnDirtyStagedFiles: true
141+
warning: <WarningBox title="Warning" content="Warning content."></WarningBox>
143142
};
144143
const component = shallow(<CommitBox {...props} />);
145144
expect(component.find(WarningBox).length).toEqual(1);

tests/test-components/GitPanel.spec.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -254,7 +254,7 @@ describe('GitPanel', () => {
254254
notifyRemoteChanges: {
255255
connect: jest.fn()
256256
},
257-
dirtyStagedFilesStatusChanged: {
257+
dirtyFilesStatusChanged: {
258258
connect: jest.fn()
259259
}
260260
} as any;

0 commit comments

Comments
 (0)