Skip to content

Commit 20d9627

Browse files
ianhifcollonval
andauthored
show both staged and unstaged changes for a file (#629)
* show staged and unstaged changes to the same file previously if you add staged a file and then made subsequent changes to it then the file will only show up in the staged files list. This adds another option to the Status used internally that notes when a file has both staged and unstaged changes and modifies the filelist to account for this. * suggested changes * Skip checkout if file was added in index * Display 'MD' file status as partially-staged * Improve sligthly the test * Reset only staged or partially staged file * Fix unit tests * account for partial staging in _hasStagedFile and _hasUnstagedFile * allow discard on staged files in simple mode * add contextmenu to files in simple staging mode Co-authored-by: Frederic Collonval <[email protected]>
1 parent ed3cf36 commit 20d9627

File tree

6 files changed

+241
-25
lines changed

6 files changed

+241
-25
lines changed

src/components/FileItem.tsx

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -74,9 +74,9 @@ export class FileItem extends React.Component<IFileItemProps> {
7474
}
7575

7676
render() {
77-
const status =
78-
this.getFileChangedLabel(this.props.file.y as any) ||
79-
this.getFileChangedLabel(this.props.file.x as any);
77+
const { file } = this.props;
78+
const status_code = file.status === 'staged' ? file.x : file.y;
79+
const status = this.getFileChangedLabel(status_code as any);
8080

8181
return (
8282
<li
@@ -110,9 +110,7 @@ export class FileItem extends React.Component<IFileItemProps> {
110110
/>
111111
{this.props.actions}
112112
<span className={this.getFileChangedLabelClass(this.props.file.y)}>
113-
{this.props.file.y === '?'
114-
? 'U'
115-
: this.props.file.y.trim() || this.props.file.x}
113+
{this.props.file.y === '?' ? 'U' : status_code}
116114
</span>
117115
</li>
118116
);

src/components/FileList.tsx

Lines changed: 77 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@ export class FileList extends React.Component<IFileListProps, IFileListState> {
4444
this._contextMenuStaged = new Menu({ commands });
4545
this._contextMenuUnstaged = new Menu({ commands });
4646
this._contextMenuUntracked = new Menu({ commands });
47+
this._contextMenuSimpleUntracked = new Menu({ commands });
48+
this._contextMenuSimpleTracked = new Menu({ commands });
4749

4850
this.state = {
4951
selectedFile: null
@@ -134,7 +136,7 @@ export class FileList extends React.Component<IFileListProps, IFileListState> {
134136
label: 'Discard',
135137
caption: 'Discard recent changes of selected file',
136138
execute: () => {
137-
this.discardChanges(this.state.selectedFile.to);
139+
this.discardChanges(this.state.selectedFile);
138140
}
139141
});
140142
}
@@ -159,6 +161,18 @@ export class FileList extends React.Component<IFileListProps, IFileListState> {
159161
[CommandIDs.gitFileOpen, CommandIDs.gitFileTrack].forEach(command => {
160162
this._contextMenuUntracked.addItem({ command });
161163
});
164+
165+
[
166+
CommandIDs.gitFileOpen,
167+
CommandIDs.gitFileDiscard,
168+
CommandIDs.gitFileDiffWorking
169+
].forEach(command => {
170+
this._contextMenuSimpleTracked.addItem({ command });
171+
});
172+
173+
[CommandIDs.gitFileOpen].forEach(command => {
174+
this._contextMenuSimpleUntracked.addItem({ command });
175+
});
162176
}
163177

164178
/** Handle right-click on a staged file */
@@ -179,6 +193,18 @@ export class FileList extends React.Component<IFileListProps, IFileListState> {
179193
this._contextMenuUntracked.open(event.clientX, event.clientY);
180194
};
181195

196+
/** Handle right-click on an untracked file in Simple mode*/
197+
contextMenuSimpleUntracked = (event: React.MouseEvent) => {
198+
event.preventDefault();
199+
this._contextMenuSimpleUntracked.open(event.clientX, event.clientY);
200+
};
201+
202+
/** Handle right-click on an tracked file in Simple mode*/
203+
contextMenuSimpleTracked = (event: React.MouseEvent) => {
204+
event.preventDefault();
205+
this._contextMenuSimpleTracked.open(event.clientX, event.clientY);
206+
};
207+
182208
/** Reset all staged files */
183209
resetAllStagedFiles = async () => {
184210
await this.props.model.reset();
@@ -234,23 +260,31 @@ export class FileList extends React.Component<IFileListProps, IFileListState> {
234260
};
235261

236262
/** Discard changes in a specific unstaged or staged file */
237-
discardChanges = async (file: string) => {
263+
discardChanges = async (file: Git.IStatusFile) => {
238264
const result = await showDialog({
239265
title: 'Discard changes',
240266
body: (
241267
<span>
242-
Are you sure you want to permanently discard changes to <b>{file}</b>?
243-
This action cannot be undone.
268+
Are you sure you want to permanently discard changes to{' '}
269+
<b>{file.to}</b>? This action cannot be undone.
244270
</span>
245271
),
246272
buttons: [Dialog.cancelButton(), Dialog.warnButton({ label: 'Discard' })]
247273
});
248274
if (result.button.accept) {
249275
try {
250-
await this.props.model.reset(file);
251-
await this.props.model.checkout({ filename: file });
276+
if (file.status === 'staged' || file.status === 'partially-staged') {
277+
await this.props.model.reset(file.to);
278+
}
279+
if (
280+
file.status === 'unstaged' ||
281+
(file.status === 'partially-staged' && file.x !== 'A')
282+
) {
283+
// resetting an added file moves it to untracked category => checkout will fail
284+
await this.props.model.checkout({ filename: file.to });
285+
}
252286
} catch (reason) {
253-
showErrorMessage(`Discard changes for ${file} failed.`, reason, [
287+
showErrorMessage(`Discard changes for ${file.to} failed.`, reason, [
254288
Dialog.warnButton({ label: 'DISMISS' })
255289
]);
256290
}
@@ -297,6 +331,16 @@ export class FileList extends React.Component<IFileListProps, IFileListState> {
297331
case 'untracked':
298332
untrackedFiles.push(file);
299333
break;
334+
case 'partially-staged':
335+
stagedFiles.push({
336+
...file,
337+
status: 'staged'
338+
});
339+
unstagedFiles.push({
340+
...file,
341+
status: 'unstaged'
342+
});
343+
break;
300344

301345
default:
302346
break;
@@ -325,7 +369,8 @@ export class FileList extends React.Component<IFileListProps, IFileListState> {
325369
this.state.selectedFile.x === candidate.x &&
326370
this.state.selectedFile.y === candidate.y &&
327371
this.state.selectedFile.from === candidate.from &&
328-
this.state.selectedFile.to === candidate.to
372+
this.state.selectedFile.to === candidate.to &&
373+
this.state.selectedFile.status === candidate.status
329374
);
330375
}
331376

@@ -443,7 +488,7 @@ export class FileList extends React.Component<IFileListProps, IFileListState> {
443488
iconName={'git-discard'}
444489
title={'Discard changes'}
445490
onClick={() => {
446-
this.discardChanges(file.to);
491+
this.discardChanges(file);
447492
}}
448493
/>
449494
<ActionButton
@@ -569,9 +614,13 @@ export class FileList extends React.Component<IFileListProps, IFileListState> {
569614
? (): void => undefined
570615
: openFile;
571616

572-
let diffButton: JSX.Element;
573-
if (file.status === 'unstaged') {
574-
diffButton = this._createDiffButton(file, 'WORKING');
617+
let contextMenu = this.contextMenuSimpleUntracked;
618+
619+
if (
620+
file.status === 'unstaged' ||
621+
file.status === 'partially-staged'
622+
) {
623+
const diffButton = this._createDiffButton(file, 'WORKING');
575624
actions = (
576625
<React.Fragment>
577626
<ActionButton
@@ -586,7 +635,7 @@ export class FileList extends React.Component<IFileListProps, IFileListState> {
586635
iconName={'git-discard'}
587636
title={'Discard changes'}
588637
onClick={() => {
589-
this.discardChanges(file.to);
638+
this.discardChanges(file);
590639
}}
591640
/>
592641
</React.Fragment>
@@ -596,8 +645,9 @@ export class FileList extends React.Component<IFileListProps, IFileListState> {
596645
? () => this._openDiffView(file, 'WORKING')
597646
: () => undefined
598647
: openFile;
648+
contextMenu = this.contextMenuSimpleTracked;
599649
} else if (file.status === 'staged') {
600-
diffButton = this._createDiffButton(file, 'INDEX');
650+
const diffButton = this._createDiffButton(file, 'INDEX');
601651
actions = (
602652
<React.Fragment>
603653
<ActionButton
@@ -607,13 +657,22 @@ export class FileList extends React.Component<IFileListProps, IFileListState> {
607657
onClick={openFile}
608658
/>
609659
{diffButton}
660+
<ActionButton
661+
className={hiddenButtonStyle}
662+
iconName={'git-discard'}
663+
title={'Discard changes'}
664+
onClick={() => {
665+
this.discardChanges(file);
666+
}}
667+
/>
610668
</React.Fragment>
611669
);
612670
onDoubleClick = doubleClickDiff
613671
? diffButton
614672
? () => this._openDiffView(file, 'INDEX')
615673
: () => undefined
616674
: openFile;
675+
contextMenu = this.contextMenuSimpleTracked;
617676
}
618677

619678
return (
@@ -624,6 +683,8 @@ export class FileList extends React.Component<IFileListProps, IFileListState> {
624683
markBox={true}
625684
model={this.props.model}
626685
onDoubleClick={onDoubleClick}
686+
contextMenu={contextMenu}
687+
selectFile={this.updateSelectedFile}
627688
/>
628689
);
629690
})}
@@ -683,4 +744,6 @@ export class FileList extends React.Component<IFileListProps, IFileListState> {
683744
private _contextMenuStaged: Menu;
684745
private _contextMenuUnstaged: Menu;
685746
private _contextMenuUntracked: Menu;
747+
private _contextMenuSimpleTracked: Menu;
748+
private _contextMenuSimpleUntracked: Menu;
686749
}

src/components/GitPanel.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -418,11 +418,15 @@ export class GitPanel extends React.Component<
418418
}
419419

420420
private _hasStagedFile(): boolean {
421-
return this.state.files.some(file => file.status === 'staged');
421+
return this.state.files.some(
422+
file => file.status === 'staged' || file.status === 'partially-staged'
423+
);
422424
}
423425

424426
private _hasUnStagedFile(): boolean {
425-
return this.state.files.some(file => file.status === 'unstaged');
427+
return this.state.files.some(
428+
file => file.status === 'unstaged' || file.status === 'partially-staged'
429+
);
426430
}
427431

428432
/**

src/tokens.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -493,5 +493,10 @@ export namespace Git {
493493
toggle(fname: string): void;
494494
}
495495

496-
export type Status = 'untracked' | 'staged' | 'unstaged' | null;
496+
export type Status =
497+
| 'untracked'
498+
| 'staged'
499+
| 'unstaged'
500+
| 'partially-staged'
501+
| null;
497502
}

src/utils.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,8 @@ export function decodeStage(x: string, y: string): Git.Status {
4040
return 'untracked';
4141
} else {
4242
// If file is staged
43-
if (x !== ' ' && y !== 'D') {
44-
return 'staged';
43+
if (x !== ' ') {
44+
return y !== ' ' ? 'partially-staged' : 'staged';
4545
}
4646
// If file is unstaged but tracked
4747
if (y !== ' ') {

0 commit comments

Comments
 (0)