Skip to content

Commit

Permalink
show both staged and unstaged changes for a file (#629)
Browse files Browse the repository at this point in the history
* 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 <fcollonval@gmail.com>
  • Loading branch information
ianhi and fcollonval committed Jun 14, 2020
1 parent ed3cf36 commit 20d9627
Show file tree
Hide file tree
Showing 6 changed files with 241 additions and 25 deletions.
10 changes: 4 additions & 6 deletions src/components/FileItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -74,9 +74,9 @@ export class FileItem extends React.Component<IFileItemProps> {
}

render() {
const status =
this.getFileChangedLabel(this.props.file.y as any) ||
this.getFileChangedLabel(this.props.file.x as any);
const { file } = this.props;
const status_code = file.status === 'staged' ? file.x : file.y;
const status = this.getFileChangedLabel(status_code as any);

return (
<li
Expand Down Expand Up @@ -110,9 +110,7 @@ export class FileItem extends React.Component<IFileItemProps> {
/>
{this.props.actions}
<span className={this.getFileChangedLabelClass(this.props.file.y)}>
{this.props.file.y === '?'
? 'U'
: this.props.file.y.trim() || this.props.file.x}
{this.props.file.y === '?' ? 'U' : status_code}
</span>
</li>
);
Expand Down
91 changes: 77 additions & 14 deletions src/components/FileList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ export class FileList extends React.Component<IFileListProps, IFileListState> {
this._contextMenuStaged = new Menu({ commands });
this._contextMenuUnstaged = new Menu({ commands });
this._contextMenuUntracked = new Menu({ commands });
this._contextMenuSimpleUntracked = new Menu({ commands });
this._contextMenuSimpleTracked = new Menu({ commands });

this.state = {
selectedFile: null
Expand Down Expand Up @@ -134,7 +136,7 @@ export class FileList extends React.Component<IFileListProps, IFileListState> {
label: 'Discard',
caption: 'Discard recent changes of selected file',
execute: () => {
this.discardChanges(this.state.selectedFile.to);
this.discardChanges(this.state.selectedFile);
}
});
}
Expand All @@ -159,6 +161,18 @@ export class FileList extends React.Component<IFileListProps, IFileListState> {
[CommandIDs.gitFileOpen, CommandIDs.gitFileTrack].forEach(command => {
this._contextMenuUntracked.addItem({ command });
});

[
CommandIDs.gitFileOpen,
CommandIDs.gitFileDiscard,
CommandIDs.gitFileDiffWorking
].forEach(command => {
this._contextMenuSimpleTracked.addItem({ command });
});

[CommandIDs.gitFileOpen].forEach(command => {
this._contextMenuSimpleUntracked.addItem({ command });
});
}

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

/** Handle right-click on an untracked file in Simple mode*/
contextMenuSimpleUntracked = (event: React.MouseEvent) => {
event.preventDefault();
this._contextMenuSimpleUntracked.open(event.clientX, event.clientY);
};

/** Handle right-click on an tracked file in Simple mode*/
contextMenuSimpleTracked = (event: React.MouseEvent) => {
event.preventDefault();
this._contextMenuSimpleTracked.open(event.clientX, event.clientY);
};

/** Reset all staged files */
resetAllStagedFiles = async () => {
await this.props.model.reset();
Expand Down Expand Up @@ -234,23 +260,31 @@ export class FileList extends React.Component<IFileListProps, IFileListState> {
};

/** Discard changes in a specific unstaged or staged file */
discardChanges = async (file: string) => {
discardChanges = async (file: Git.IStatusFile) => {
const result = await showDialog({
title: 'Discard changes',
body: (
<span>
Are you sure you want to permanently discard changes to <b>{file}</b>?
This action cannot be undone.
Are you sure you want to permanently discard changes to{' '}
<b>{file.to}</b>? This action cannot be undone.
</span>
),
buttons: [Dialog.cancelButton(), Dialog.warnButton({ label: 'Discard' })]
});
if (result.button.accept) {
try {
await this.props.model.reset(file);
await this.props.model.checkout({ filename: file });
if (file.status === 'staged' || file.status === 'partially-staged') {
await this.props.model.reset(file.to);
}
if (
file.status === 'unstaged' ||
(file.status === 'partially-staged' && file.x !== 'A')
) {
// resetting an added file moves it to untracked category => checkout will fail
await this.props.model.checkout({ filename: file.to });
}
} catch (reason) {
showErrorMessage(`Discard changes for ${file} failed.`, reason, [
showErrorMessage(`Discard changes for ${file.to} failed.`, reason, [
Dialog.warnButton({ label: 'DISMISS' })
]);
}
Expand Down Expand Up @@ -297,6 +331,16 @@ export class FileList extends React.Component<IFileListProps, IFileListState> {
case 'untracked':
untrackedFiles.push(file);
break;
case 'partially-staged':
stagedFiles.push({
...file,
status: 'staged'
});
unstagedFiles.push({
...file,
status: 'unstaged'
});
break;

default:
break;
Expand Down Expand Up @@ -325,7 +369,8 @@ export class FileList extends React.Component<IFileListProps, IFileListState> {
this.state.selectedFile.x === candidate.x &&
this.state.selectedFile.y === candidate.y &&
this.state.selectedFile.from === candidate.from &&
this.state.selectedFile.to === candidate.to
this.state.selectedFile.to === candidate.to &&
this.state.selectedFile.status === candidate.status
);
}

Expand Down Expand Up @@ -443,7 +488,7 @@ export class FileList extends React.Component<IFileListProps, IFileListState> {
iconName={'git-discard'}
title={'Discard changes'}
onClick={() => {
this.discardChanges(file.to);
this.discardChanges(file);
}}
/>
<ActionButton
Expand Down Expand Up @@ -569,9 +614,13 @@ export class FileList extends React.Component<IFileListProps, IFileListState> {
? (): void => undefined
: openFile;

let diffButton: JSX.Element;
if (file.status === 'unstaged') {
diffButton = this._createDiffButton(file, 'WORKING');
let contextMenu = this.contextMenuSimpleUntracked;

if (
file.status === 'unstaged' ||
file.status === 'partially-staged'
) {
const diffButton = this._createDiffButton(file, 'WORKING');
actions = (
<React.Fragment>
<ActionButton
Expand All @@ -586,7 +635,7 @@ export class FileList extends React.Component<IFileListProps, IFileListState> {
iconName={'git-discard'}
title={'Discard changes'}
onClick={() => {
this.discardChanges(file.to);
this.discardChanges(file);
}}
/>
</React.Fragment>
Expand All @@ -596,8 +645,9 @@ export class FileList extends React.Component<IFileListProps, IFileListState> {
? () => this._openDiffView(file, 'WORKING')
: () => undefined
: openFile;
contextMenu = this.contextMenuSimpleTracked;
} else if (file.status === 'staged') {
diffButton = this._createDiffButton(file, 'INDEX');
const diffButton = this._createDiffButton(file, 'INDEX');
actions = (
<React.Fragment>
<ActionButton
Expand All @@ -607,13 +657,22 @@ export class FileList extends React.Component<IFileListProps, IFileListState> {
onClick={openFile}
/>
{diffButton}
<ActionButton
className={hiddenButtonStyle}
iconName={'git-discard'}
title={'Discard changes'}
onClick={() => {
this.discardChanges(file);
}}
/>
</React.Fragment>
);
onDoubleClick = doubleClickDiff
? diffButton
? () => this._openDiffView(file, 'INDEX')
: () => undefined
: openFile;
contextMenu = this.contextMenuSimpleTracked;
}

return (
Expand All @@ -624,6 +683,8 @@ export class FileList extends React.Component<IFileListProps, IFileListState> {
markBox={true}
model={this.props.model}
onDoubleClick={onDoubleClick}
contextMenu={contextMenu}
selectFile={this.updateSelectedFile}
/>
);
})}
Expand Down Expand Up @@ -683,4 +744,6 @@ export class FileList extends React.Component<IFileListProps, IFileListState> {
private _contextMenuStaged: Menu;
private _contextMenuUnstaged: Menu;
private _contextMenuUntracked: Menu;
private _contextMenuSimpleTracked: Menu;
private _contextMenuSimpleUntracked: Menu;
}
8 changes: 6 additions & 2 deletions src/components/GitPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -418,11 +418,15 @@ export class GitPanel extends React.Component<
}

private _hasStagedFile(): boolean {
return this.state.files.some(file => file.status === 'staged');
return this.state.files.some(
file => file.status === 'staged' || file.status === 'partially-staged'
);
}

private _hasUnStagedFile(): boolean {
return this.state.files.some(file => file.status === 'unstaged');
return this.state.files.some(
file => file.status === 'unstaged' || file.status === 'partially-staged'
);
}

/**
Expand Down
7 changes: 6 additions & 1 deletion src/tokens.ts
Original file line number Diff line number Diff line change
Expand Up @@ -493,5 +493,10 @@ export namespace Git {
toggle(fname: string): void;
}

export type Status = 'untracked' | 'staged' | 'unstaged' | null;
export type Status =
| 'untracked'
| 'staged'
| 'unstaged'
| 'partially-staged'
| null;
}
4 changes: 2 additions & 2 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,8 @@ export function decodeStage(x: string, y: string): Git.Status {
return 'untracked';
} else {
// If file is staged
if (x !== ' ' && y !== 'D') {
return 'staged';
if (x !== ' ') {
return y !== ' ' ? 'partially-staged' : 'staged';
}
// If file is unstaged but tracked
if (y !== ' ') {
Expand Down
Loading

0 comments on commit 20d9627

Please sign in to comment.