@jupyterlab/git
Version:
A JupyterLab extension for version control using git
836 lines (835 loc) • 40.5 kB
JavaScript
import { Dialog, showDialog, showErrorMessage } from '@jupyterlab/apputils';
import { Menu } from '@lumino/widgets';
import { Signal } from '@lumino/signaling';
import * as React from 'react';
import AutoSizer from 'react-virtualized-auto-sizer';
import { addMenuItems } from '../commandsAndMenu';
import { getDiffProvider } from '../model';
import { hiddenButtonStyle } from '../style/ActionButtonStyle';
import { fileListWrapperClass } from '../style/FileListStyle';
import { addIcon, diffIcon, discardIcon, openIcon, removeIcon, rewindIcon } from '../style/icons';
import { ContextCommandIDs, CommandIDs } from '../tokens';
import { ActionButton } from './ActionButton';
import { FileItem } from './FileItem';
import { GitStage } from './GitStage';
import { discardAllChanges } from '../widgets/discardAllChanges';
import { SelectAllButton } from './SelectAllButton';
import { stopPropagationWrapper } from '../utils';
export const CONTEXT_COMMANDS = {
'partially-staged': [
ContextCommandIDs.gitFileOpen,
ContextCommandIDs.gitFileUnstage,
ContextCommandIDs.gitFileDiff,
ContextCommandIDs.gitFileHistory
],
'remote-changed': [ContextCommandIDs.gitFileOpen],
unstaged: [
ContextCommandIDs.gitFileOpen,
ContextCommandIDs.gitFileStage,
ContextCommandIDs.gitFileDiscard,
ContextCommandIDs.gitFileDiff,
ContextCommandIDs.gitFileHistory
],
untracked: [
ContextCommandIDs.gitFileOpen,
ContextCommandIDs.gitFileTrack,
ContextCommandIDs.gitIgnore,
ContextCommandIDs.gitIgnoreExtension,
ContextCommandIDs.gitFileDelete
],
staged: [
ContextCommandIDs.gitFileOpen,
ContextCommandIDs.gitFileUnstage,
ContextCommandIDs.gitFileDiff,
ContextCommandIDs.gitCommitAmendStaged,
ContextCommandIDs.gitFileHistory
],
unmodified: [ContextCommandIDs.gitFileHistory],
unmerged: [ContextCommandIDs.gitFileDiff],
stashed: [ContextCommandIDs.gitFileStashPop]
};
const SIMPLE_CONTEXT_COMMANDS = {
'partially-staged': [
ContextCommandIDs.gitFileOpen,
ContextCommandIDs.gitFileDiscard,
ContextCommandIDs.gitFileDiff,
ContextCommandIDs.gitFileHistory
],
'remote-changed': [ContextCommandIDs.gitFileOpen],
staged: [
ContextCommandIDs.gitFileOpen,
ContextCommandIDs.gitFileDiscard,
ContextCommandIDs.gitFileDiff,
ContextCommandIDs.gitFileHistory
],
unstaged: [
ContextCommandIDs.gitFileOpen,
ContextCommandIDs.gitFileDiscard,
ContextCommandIDs.gitFileDiff,
ContextCommandIDs.gitFileHistory
],
untracked: [
ContextCommandIDs.gitFileOpen,
ContextCommandIDs.gitIgnore,
ContextCommandIDs.gitIgnoreExtension,
ContextCommandIDs.gitFileDelete
],
unmodified: [ContextCommandIDs.gitFileHistory],
unmerged: [ContextCommandIDs.gitFileDiff],
stashed: [ContextCommandIDs.gitFileStashPop]
};
/**
* Compare fileA and fileB.
* @param fileA
* @param fileB
* @returns true if fileA and fileB are equal, otherwise, false.
*/
const areFilesEqual = (fileA, fileB) => {
return (fileA.x === fileB.x &&
fileA.y === fileB.y &&
fileA.from === fileB.from &&
fileA.to === fileB.to &&
fileA.status === fileB.status);
};
export class FileList extends React.Component {
constructor(props) {
super(props);
/**
* Open the context menu on the advanced view
*
* @param selectedFile The file on which the context menu is opened
* @param event The click event
*/
this.openContextMenu = (selectedFile, event) => {
event.preventDefault();
let selectedFiles;
if (!this._isSelectedFile(selectedFile)) {
this._selectOnlyOneFile(selectedFile);
selectedFiles = [selectedFile];
}
else {
selectedFiles = this.state.selectedFiles;
}
const contextMenu = new Menu({ commands: this.props.commands });
// @ts-expect-error unproper index
const commands = CONTEXT_COMMANDS[selectedFiles[0].status];
addMenuItems(commands, contextMenu, selectedFiles);
contextMenu.open(event.clientX, event.clientY);
};
/**
* Open the context menu on the simple view
*
* @param selectedFile The file on which the context menu is opened
* @param event The click event
*/
this.openSimpleContextMenu = (selectedFile, event) => {
event.preventDefault();
const contextMenu = new Menu({ commands: this.props.commands });
// @ts-expect-error unproper index
const commands = SIMPLE_CONTEXT_COMMANDS[selectedFile.status];
addMenuItems(commands, contextMenu, [selectedFile]);
contextMenu.open(event.clientX, event.clientY);
};
/** Reset all staged files */
this.resetAllStagedFiles = async (event) => {
event === null || event === void 0 ? void 0 : event.stopPropagation();
await this.props.model.reset();
};
/** Reset staged selected files */
this.resetSelectedFiles = (file) => {
if (this._isSelectedFile(file)) {
this.state.selectedFiles.forEach(file => this.props.model.reset(file.to));
}
else {
this.props.model.reset(file.to);
}
};
/** If the clicked file is selected, open all selected files.
* If the clicked file is not selected, open the clicked file only.
*/
this.openSelectedFiles = (clickedFile) => {
if (this._isSelectedFile(clickedFile)) {
this.props.commands.execute(ContextCommandIDs.gitFileOpen, {
files: this.state.selectedFiles
});
}
else {
this.props.commands.execute(ContextCommandIDs.gitFileOpen, {
files: [clickedFile]
});
}
};
/** Add all unstaged files */
this.addAllUnstagedFiles = async (event) => {
event === null || event === void 0 ? void 0 : event.stopPropagation();
await this.props.model.addAllUnstaged();
};
/** Discard changes in all unstaged files */
this.discardAllUnstagedFiles = async (event) => {
event === null || event === void 0 ? void 0 : event.stopPropagation();
const result = await showDialog({
title: this.props.trans.__('Discard all changes'),
body: this.props.trans.__('Are you sure you want to permanently discard changes to all unstaged files? This action cannot be undone.'),
buttons: [
Dialog.cancelButton({ label: this.props.trans.__('Cancel') }),
Dialog.warnButton({ label: this.props.trans.__('Discard') })
]
});
if (result.button.accept) {
try {
await this.props.model.checkout();
}
catch (reason) {
showErrorMessage(this.props.trans.__('Discard all unstaged changes failed.'), reason);
}
}
};
/** Discard changes in all unstaged and staged files */
this.discardAllChanges = async (event) => {
event === null || event === void 0 ? void 0 : event.stopPropagation();
await discardAllChanges(this.props.model, this.props.trans);
};
/** Add a specific unstaged file */
this.addFile = async (...file) => {
await this.props.model.add(...file);
};
/** Discard changes in a specific unstaged or staged file */
this.discardChanges = (file) => {
if (this._isSelectedFile(file)) {
this.props.commands.execute(ContextCommandIDs.gitFileDiscard, {
files: this.state.selectedFiles
});
}
else {
this.props.commands.execute(ContextCommandIDs.gitFileDiscard, {
files: [file]
});
}
};
/** Add all untracked files */
this.addAllUntrackedFiles = async (event) => {
event === null || event === void 0 ? void 0 : event.stopPropagation();
await this.props.model.addAllUntracked();
};
this.addAllMarkedFiles = async () => {
await this.addFile(...this.markedFiles.map(file => file.to));
};
/**
* Select files into state.selectedFiles
* @param file The current cliced-on file
* @param options Selection options
*/
this.setSelection = (file, options) => {
if (options && options.singleton) {
this._selectOnlyOneFile(file);
}
if (options && options.group) {
this._selectUntilFile(file);
}
if (!options) {
this._toggleFile(file);
}
};
/**
* Mark files from the latest selected to this one
*
* @param file The current clicked-on file
*/
this.markUntilFile = (file) => {
if (!this.state.lastClickedFile) {
this.props.model.setMark(file.to, true);
return;
}
const filesWithMarkBox = this.props.files.filter(fileStatus => !['unmerged', 'remote-changed'].includes(fileStatus.status));
const lastClickedFileIndex = filesWithMarkBox.findIndex(fileStatus => {
var _a;
return areFilesEqual(fileStatus, (_a = this.state.lastClickedFile) !== null && _a !== void 0 ? _a : {});
});
const currentFileIndex = filesWithMarkBox.findIndex(fileStatus => areFilesEqual(fileStatus, file));
if (currentFileIndex > lastClickedFileIndex) {
const filesToAdd = filesWithMarkBox.slice(lastClickedFileIndex, currentFileIndex + 1);
filesToAdd.forEach(f => this.props.model.setMark(f.to, true));
}
else {
const filesToAdd = filesWithMarkBox.slice(currentFileIndex, lastClickedFileIndex + 1);
filesToAdd.forEach(f => this.props.model.setMark(f.to, true));
}
};
/**
* Set mark status from select-all button
*
* @param files Files to toggle
*/
this.toggleAllFiles = (files) => {
const areFilesAllMarked = this._areFilesAllMarked();
files.forEach(f => this.props.model.setMark(f.to, !areFilesAllMarked));
};
this._selectOnlyOneFile = (file) => {
this.setState({
selectedFiles: [file],
lastClickedFile: file
});
};
/**
* Toggle selection status of a file
* @param file The clicked file
*/
this._toggleFile = (file) => {
var _a;
if (file.status !== ((_a = this.state.lastClickedFile) === null || _a === void 0 ? void 0 : _a.status)) {
this._selectOnlyOneFile(file);
return;
}
const fileStatus = this.state.selectedFiles.find(fileStatus => areFilesEqual(fileStatus, file));
if (!fileStatus) {
this.setState({
selectedFiles: [...this.state.selectedFiles, file],
lastClickedFile: file
});
}
else {
this.setState({
selectedFiles: this.state.selectedFiles.filter(fileStatus => !areFilesEqual(fileStatus, file)),
lastClickedFile: file
});
}
};
/**
* Select a list of files
* @param files List of files to select
*/
this._selectFiles = (files) => {
this.setState(prevState => {
return {
selectedFiles: [
...prevState.selectedFiles,
...files.filter(file => !prevState.selectedFiles.some(f => areFilesEqual(f, file)))
]
};
});
};
/**
* Deselect a list of file
* @param files List of file to deselect
*/
this._deselectFiles = (files) => {
this.setState(prevState => {
return {
selectedFiles: prevState.selectedFiles.filter(selectedFile => !files.some(file => areFilesEqual(selectedFile, file)))
};
});
};
/**
* Handle shift-click behaviour for file selection
* @param file The shift-clicked file
*/
this._selectUntilFile = (file) => {
if (!this.state.lastClickedFile ||
file.status !== this.state.lastClickedFile.status) {
this._selectOnlyOneFile(file);
return;
}
const selectedFileStatus = this.state.lastClickedFile.status;
const allFilesWithSelectedStatus = this.props.files.filter(fileStatus => fileStatus.status === selectedFileStatus);
const partiallyStagedFiles = this.props.files.filter(fileStatus => fileStatus.status === 'partially-staged');
switch (selectedFileStatus) {
case 'staged':
allFilesWithSelectedStatus.push(...partiallyStagedFiles.map(fileStatus => ({
...fileStatus,
status: 'staged'
})));
break;
case 'unstaged':
allFilesWithSelectedStatus.push(...partiallyStagedFiles.map(fileStatus => ({
...fileStatus,
status: 'unstaged'
})));
break;
}
allFilesWithSelectedStatus.sort((a, b) => a.to.localeCompare(b.to));
const lastClickedFileIndex = allFilesWithSelectedStatus.findIndex(fileStatus => {
var _a;
return areFilesEqual(fileStatus, (_a = this.state.lastClickedFile) !== null && _a !== void 0 ? _a : {});
});
const currentFileIndex = allFilesWithSelectedStatus.findIndex(fileStatus => areFilesEqual(fileStatus, file));
if (currentFileIndex > lastClickedFileIndex) {
const highestSelectedIndex = allFilesWithSelectedStatus.findIndex((file, index) => index > lastClickedFileIndex && !this._isSelectedFile(file));
if (highestSelectedIndex === -1) {
this._deselectFiles(allFilesWithSelectedStatus.slice(currentFileIndex + 1));
}
else if (currentFileIndex < highestSelectedIndex) {
this._deselectFiles(allFilesWithSelectedStatus.slice(currentFileIndex + 1, highestSelectedIndex));
}
else {
this._selectFiles(allFilesWithSelectedStatus.slice(highestSelectedIndex, currentFileIndex + 1));
}
}
else if (currentFileIndex < lastClickedFileIndex) {
const lowestSelectedIndex = allFilesWithSelectedStatus.findIndex((file, index) => index < lastClickedFileIndex && this._isSelectedFile(file));
if (lowestSelectedIndex === -1) {
this._selectFiles(allFilesWithSelectedStatus.slice(currentFileIndex, lastClickedFileIndex));
}
else if (currentFileIndex < lowestSelectedIndex) {
this._selectFiles(allFilesWithSelectedStatus.slice(currentFileIndex, lowestSelectedIndex));
}
else {
this._deselectFiles(allFilesWithSelectedStatus.slice(lowestSelectedIndex, currentFileIndex));
}
}
else {
this._selectOnlyOneFile(file);
}
};
this.pullFromRemote = async (event) => {
await this.props.commands.execute(CommandIDs.gitPull, {});
};
/**
* Render an unmerged file
*
* Note: This is actually a React.FunctionComponent but defined as
* a private method as it needs access to FileList properties.
*
* @param rowProps Row properties
*/
this._renderUnmergedRow = (rowProps) => {
const action = this.props.settings.get('fileClickAction')
.composite;
const { data, index, style } = rowProps;
const file = data[index];
const diffButton = this._createDiffButton(file);
const openDiff = () => {
void this._openDiffViews([file]);
};
// Conflicted files only resolve through the diff view, so double-click
// opens the diff for every non-`select-only` action.
return (React.createElement(FileItem, { trans: this.props.trans, actions: !file.is_binary ? diffButton : null, contextMenu: this.openContextMenu, file: file, model: this.props.model, selected: this._isSelectedFile(file), setSelection: this.setSelection, onClick: action === 'diff-on-single' ? openDiff : undefined, onDoubleClick: action === 'select-only' ? () => undefined : openDiff, style: { ...style } }));
};
/**
* Render a staged file
*
* Note: This is actually a React.FunctionComponent but defined as
* a private method as it needs access to FileList properties.
*
* @param rowProps Row properties
*/
this._renderStagedRow = (rowProps) => {
const action = this.props.settings.get('fileClickAction')
.composite;
const { data, index, style } = rowProps;
const file = data[index];
const diffButton = this._createDiffButton(file);
return (React.createElement(FileItem, { trans: this.props.trans, actions: React.createElement(React.Fragment, null,
React.createElement(ActionButton, { className: hiddenButtonStyle, icon: openIcon, title: this.props.trans.__('Open this file'), onClick: stopPropagationWrapper(() => this.openSelectedFiles(file)) }),
diffButton,
React.createElement(ActionButton, { className: hiddenButtonStyle, icon: removeIcon, title: this.props.trans.__('Unstage this change'), onClick: stopPropagationWrapper(() => {
this.resetSelectedFiles(file);
}) })), file: file, contextMenu: this.openContextMenu, model: this.props.model, selected: this._isSelectedFile(file), setSelection: this.setSelection, onClick: this._stagedOrChangedSingleClick(action, file, diffButton), onDoubleClick: this._stagedOrChangedDoubleClick(action, file, diffButton), style: style }));
};
/**
* Render a changed file
*
* Note: This is actually a React.FunctionComponent but defined as
* a private method as it needs access to FileList properties.
*
* @param rowProps Row properties
*/
this._renderChangedRow = (rowProps) => {
const action = this.props.settings.get('fileClickAction')
.composite;
const { data, index, style } = rowProps;
const file = data[index];
const diffButton = this._createDiffButton(file);
return (React.createElement(FileItem, { trans: this.props.trans, actions: React.createElement(React.Fragment, null,
React.createElement(ActionButton, { className: hiddenButtonStyle, icon: openIcon, title: this.props.trans.__('Open this file'), onClick: stopPropagationWrapper(() => this.openSelectedFiles(file)) }),
diffButton,
React.createElement(ActionButton, { className: hiddenButtonStyle, icon: discardIcon, title: this.props.trans.__('Discard changes'), onClick: stopPropagationWrapper(() => {
this.discardChanges(file);
}) }),
React.createElement(ActionButton, { className: hiddenButtonStyle, icon: addIcon, title: this.props.trans.__('Stage this change'), onClick: stopPropagationWrapper(() => {
if (this._isSelectedFile(file)) {
this.addFile(...this.state.selectedFiles.map(selectedFile => selectedFile.to));
}
else {
this.addFile(file.to);
}
}) })), file: file, contextMenu: this.openContextMenu, model: this.props.model, selected: this._isSelectedFile(file), setSelection: this.setSelection, onClick: this._stagedOrChangedSingleClick(action, file, diffButton), onDoubleClick: this._stagedOrChangedDoubleClick(action, file, diffButton), style: style }));
};
/**
* Render a untracked file.
*
* Note: This is actually a React.FunctionComponent but defined as
* a private method as it needs access to FileList properties.
*
* @param rowProps Row properties
*/
this._renderUntrackedRow = (rowProps) => {
const action = this.props.settings.get('fileClickAction')
.composite;
const { data, index, style } = rowProps;
const file = data[index];
return (React.createElement(FileItem, { trans: this.props.trans, actions: React.createElement(React.Fragment, null,
React.createElement(ActionButton, { className: hiddenButtonStyle, icon: openIcon, title: this.props.trans.__('Open this file'), onClick: stopPropagationWrapper(() => this.openSelectedFiles(file)) }),
React.createElement(ActionButton, { className: hiddenButtonStyle, icon: addIcon, title: this.props.trans.__('Track this file'), onClick: stopPropagationWrapper(() => {
if (this._isSelectedFile(file)) {
this.addFile(...this.state.selectedFiles.map(selectedFile => selectedFile.to));
}
else {
this.addFile(file.to);
}
}) })), file: file, contextMenu: this.openContextMenu, model: this.props.model, onClick: this._noDiffSingleClick(action, file), onDoubleClick: this._noDiffDoubleClick(action, file), selected: this._isSelectedFile(file), setSelection: this.setSelection, style: style }));
};
/**
* Render the remote changed list.
*
* Note: This is actually a React.FunctionComponent but defined as
* a private method as it needs access to FileList properties.
*
* @param rowProps Row properties
*/
this._renderRemoteChangedRow = (rowProps) => {
const action = this.props.settings.get('fileClickAction')
.composite;
const { data, index, style } = rowProps;
const file = data[index];
return (React.createElement(FileItem, { trans: this.props.trans, actions: React.createElement(React.Fragment, null,
React.createElement(ActionButton, { className: hiddenButtonStyle, icon: openIcon, title: this.props.trans.__('Open this file'), onClick: stopPropagationWrapper(() => this.openSelectedFiles(file)) })), file: file, contextMenu: this.openContextMenu, model: this.props.model, onClick: this._noDiffSingleClick(action, file), onDoubleClick: this._noDiffDoubleClick(action, file), selected: this._isSelectedFile(file), setSelection: this.setSelection, style: style }));
};
/**
* Render a modified file in simple mode.
*
* Note: This is actually a React.FunctionComponent but defined as
* a private method as it needs access to FileList properties.
*
* @param rowProps Row properties
*/
this._renderSimpleStageRow = (rowProps) => {
const { data, index, style } = rowProps;
const file = data[index];
const action = this.props.settings.get('fileClickAction')
.composite;
// Simple-stage rows reserve single-click for toggling the file's mark,
// so the `diff-on-single` action is mapped to the same double-click
// behaviour as `diff-on-double`.
const wantsDiff = action === 'diff-on-double' || action === 'diff-on-single';
const wantsSelectOnly = action === 'select-only';
const openFile = () => {
this.props.commands.execute(ContextCommandIDs.gitFileOpen, {
files: [file]
});
};
// Default value for actions and double click
let actions = (React.createElement(ActionButton, { className: hiddenButtonStyle, icon: openIcon, title: this.props.trans.__('Open this file'), onClick: stopPropagationWrapper(openFile) }));
let onDoubleClick = wantsSelectOnly || wantsDiff ? () => undefined : openFile;
if (file.status === 'unstaged' || file.status === 'partially-staged') {
const diffButton = this._createDiffButton(file);
actions = (React.createElement(React.Fragment, null,
React.createElement(ActionButton, { className: hiddenButtonStyle, icon: openIcon, title: this.props.trans.__('Open this file'), onClick: stopPropagationWrapper(openFile) }),
diffButton,
React.createElement(ActionButton, { className: hiddenButtonStyle, icon: discardIcon, title: this.props.trans.__('Discard changes'), onClick: stopPropagationWrapper(() => {
this.discardChanges(file);
}) })));
onDoubleClick = wantsSelectOnly
? () => undefined
: wantsDiff
? diffButton
? () => this._openDiffViews([file])
: () => undefined
: openFile;
}
else if (file.status === 'staged') {
const diffButton = this._createDiffButton(file);
actions = (React.createElement(React.Fragment, null,
React.createElement(ActionButton, { className: hiddenButtonStyle, icon: openIcon, title: this.props.trans.__('Open this file'), onClick: stopPropagationWrapper(openFile) }),
diffButton,
React.createElement(ActionButton, { className: hiddenButtonStyle, icon: discardIcon, title: this.props.trans.__('Discard changes'), onClick: stopPropagationWrapper(() => {
this.discardChanges(file);
}) })));
onDoubleClick = wantsSelectOnly
? () => undefined
: wantsDiff
? diffButton
? () => this._openDiffViews([file])
: () => undefined
: openFile;
}
const checked = this.markedFiles.some(fileStatus => areFilesEqual(fileStatus, file));
return (React.createElement(FileItem, { trans: this.props.trans, actions: actions, file: file, markBox: true, model: this.props.model, onDoubleClick: onDoubleClick, contextMenu: this.openSimpleContextMenu, setSelection: this.setSelection, style: style, markUntilFile: this.markUntilFile, checked: checked }));
};
/**
* Callback invoked upon clicking a button to stash the dirty files.
*
* @param event - event object
* @returns a promise which resolves upon stashing the latest changes
*/
this._onStashClick = async () => {
await this.props.commands.execute(CommandIDs.gitStash);
};
this.state = {
selectedFiles: [],
lastClickedFile: null,
markedFiles: props.model.markedFiles
};
}
componentDidMount() {
const { model } = this.props;
model.markChanged.connect(() => {
this.setState({ markedFiles: model.markedFiles });
}, this);
model.repositoryChanged.connect(() => {
this.setState({ markedFiles: model.markedFiles });
}, this);
}
componentWillUnmount() {
Signal.clearData(this);
}
get markedFiles() {
return this.props.model.markedFiles;
}
/**
* Render the modified files
*/
render() {
const remoteChangedFiles = [];
const unmergedFiles = [];
if (this.props.settings.composite['simpleStaging']) {
const otherFiles = [];
this.props.files.forEach(file => {
switch (file.status) {
case 'remote-changed':
remoteChangedFiles.push(file);
break;
case 'unmerged':
unmergedFiles.push(file);
break;
default:
otherFiles.push(file);
break;
}
});
return (React.createElement("div", { className: fileListWrapperClass },
React.createElement(AutoSizer, { disableWidth: true }, ({ height }) => (React.createElement(React.Fragment, null,
this._renderUnmerged(unmergedFiles, height, false),
this._renderRemoteChanged(remoteChangedFiles, height),
this._renderSimpleStage(otherFiles, height))))));
}
else {
const stagedFiles = [];
const unstagedFiles = [];
const untrackedFiles = [];
this.props.files.forEach(file => {
switch (file.status) {
case 'staged':
stagedFiles.push(file);
break;
case 'unstaged':
unstagedFiles.push(file);
break;
case 'untracked':
untrackedFiles.push(file);
break;
case 'partially-staged':
stagedFiles.push({
...file,
status: 'staged'
});
unstagedFiles.push({
...file,
status: 'unstaged'
});
break;
case 'unmerged':
unmergedFiles.push(file);
break;
case 'remote-changed':
remoteChangedFiles.push(file);
break;
default:
break;
}
});
return (React.createElement("div", { className: fileListWrapperClass, onContextMenu: event => event.preventDefault() },
React.createElement(AutoSizer, { disableWidth: true }, ({ height }) => (React.createElement(React.Fragment, null,
this._renderUnmerged(unmergedFiles, height),
this._renderRemoteChanged(remoteChangedFiles, height),
this._renderStaged(stagedFiles, height),
this._renderChanged(unstagedFiles, height),
this._renderUntracked(untrackedFiles, height))))));
}
}
/**
* Test if a file is selected
* @param candidate file to test
*/
_isSelectedFile(candidate) {
return this.state.selectedFiles.some(file => areFilesEqual(file, candidate));
}
_renderUnmerged(files, height, collapsible = true) {
// Hide section if no merge conflicts are present
return files.length > 0 ? (React.createElement(GitStage, { collapsible: collapsible, files: files, heading: this.props.trans.__('Conflicted'), height: height, rowRenderer: this._renderUnmergedRow })) : null;
}
/**
* Render the staged files list.
*
* @param files The staged files
* @param height The height of the HTML element
*/
_renderStaged(files, height) {
return (React.createElement(GitStage, { actions: React.createElement(React.Fragment, null,
React.createElement(ActionButton, { className: hiddenButtonStyle, icon: rewindIcon, onClick: this._onStashClick, title: this.props.trans.__('Stash latest changes') }),
React.createElement(ActionButton, { className: hiddenButtonStyle, disabled: files.length === 0, icon: removeIcon, title: this.props.trans.__('Unstage all changes'), onClick: this.resetAllStagedFiles })), collapsible: true, files: files, heading: this.props.trans.__('Staged'), height: height, rowRenderer: this._renderStagedRow }));
}
/**
* Render the changed files list
*
* @param files Changed files
* @param height Height of the HTML element
*/
_renderChanged(files, height) {
const disabled = files.length === 0;
return (React.createElement(GitStage, { actions: React.createElement(React.Fragment, null,
React.createElement(ActionButton, { className: hiddenButtonStyle, icon: rewindIcon, onClick: this._onStashClick, title: this.props.trans.__('Stash latest changes') }),
React.createElement(ActionButton, { className: hiddenButtonStyle, disabled: disabled, icon: discardIcon, title: this.props.trans.__('Discard All Changes'), onClick: this.discardAllUnstagedFiles }),
React.createElement(ActionButton, { className: hiddenButtonStyle, disabled: disabled, icon: addIcon, title: this.props.trans.__('Stage all changes'), onClick: this.addAllUnstagedFiles })), collapsible: true, heading: this.props.trans.__('Changed'), height: height, files: files, rowRenderer: this._renderChangedRow }));
}
/**
* Render the untracked files list.
*
* @param files Untracked files
* @param height Height of the HTML element
*/
_renderUntracked(files, height) {
return (React.createElement(GitStage, { actions: React.createElement(ActionButton, { className: hiddenButtonStyle, disabled: files.length === 0, icon: addIcon, title: this.props.trans.__('Track all untracked files'), onClick: this.addAllUntrackedFiles }), collapsible: true, heading: this.props.trans.__('Untracked'), height: height, files: files, rowRenderer: this._renderUntrackedRow }));
}
/**
* Render the a file that has changed on remote to files list.
*
* @param files Untracked files
* @param height Height of the HTML element
*/
_renderRemoteChanged(files, height) {
return (files.length > 0 && (React.createElement(GitStage, { actions: React.createElement(ActionButton, { className: hiddenButtonStyle, disabled: files.length === 0, icon: addIcon, title: this.props.trans.__('Pull from remote branch'), onClick: this.pullFromRemote }), collapsible: true, heading: this.props.trans.__('Remote Changes'), height: height, files: files, rowRenderer: this._renderRemoteChangedRow })));
}
/**
* Render the modified files in simple mode.
*
* @param files Modified files
* @param height Height of the HTML element
*/
_renderSimpleStage(files, height) {
return (React.createElement(GitStage, { selectAllButton: React.createElement(SelectAllButton, { onChange: () => {
this.toggleAllFiles(files);
}, checked: this._areFilesAllMarked() }), actions: React.createElement(ActionButton, { className: hiddenButtonStyle, disabled: files.length === 0, icon: discardIcon, title: this.props.trans.__('Discard All Changes'), onClick: this.discardAllChanges }), heading: this.props.trans.__('Changed'), height: height, files: files, rowRenderer: this._renderSimpleStageRow }));
}
/**
* Creates a button element which, depending on the settings, is used
* to either request a diff of the file, or open the file
*
* @param path File path of interest
* @param currentRef the ref to diff against the git 'HEAD' ref
*/
_createDiffButton(file) {
let handleClick;
if (this.props.settings.composite['simpleStaging']) {
handleClick = () => this._openDiffViews([file]);
}
else {
handleClick = () => {
if (this._isSelectedFile(file)) {
this._openDiffViews(this.state.selectedFiles);
}
else {
this._openDiffViews([file]);
}
};
}
return ((getDiffProvider(file.to) || !file.is_binary) && (React.createElement(ActionButton, { className: hiddenButtonStyle, icon: diffIcon, title: this.props.trans.__('Diff this file'), onClick: stopPropagationWrapper(handleClick) })));
}
/**
* Single-click handler for a Staged or Changed row, or `undefined` when
* the action doesn't trigger anything on a single click.
*/
_stagedOrChangedSingleClick(action, file, diffButton) {
if (action !== 'diff-on-single') {
return undefined;
}
return diffButton
? () => void this._openDiffViews([file])
: () => this.openSelectedFiles(file);
}
/**
* Double-click handler for a Staged or Changed row. A no-op for
* `diff-on-single` since the single-click handler already runs on the
* way to a double-click.
*/
_stagedOrChangedDoubleClick(action, file, diffButton) {
switch (action) {
case 'open-on-double':
return () => this.openSelectedFiles(file);
case 'diff-on-double':
return diffButton
? () => void this._openDiffViews([file])
: () => undefined;
case 'diff-on-single':
case 'select-only':
default:
return () => undefined;
}
}
/**
* Single-click handler for Untracked or Remote-Changed rows, where no
* diff is available; `diff-on-single` opens the file instead.
*/
_noDiffSingleClick(action, file) {
if (action !== 'diff-on-single') {
return undefined;
}
return () => {
this.props.commands.execute(ContextCommandIDs.gitFileOpen, {
files: [file]
});
};
}
/**
* Double-click handler for Untracked or Remote-Changed rows; only
* `open-on-double` opens the file, every other action is a no-op.
*/
_noDiffDoubleClick(action, file) {
if (action !== 'open-on-double') {
return () => undefined;
}
return () => {
this.props.commands.execute(ContextCommandIDs.gitFileOpen, {
files: [file]
});
};
}
/**
* Returns a callback which opens a diff of the file
*
* @param file File to open diff for
* @param currentRef the ref to diff against the git 'HEAD' ref
*/
async _openDiffViews(files) {
try {
await this.props.commands.execute(ContextCommandIDs.gitFileDiff, {
files: files.map(file => ({
filePath: file.to,
isText: !file.is_binary,
status: file.status
}))
});
}
catch (reason) {
console.error(`Failed to open diff views.\n${reason}`);
}
}
/**
* Determine if files in simple staging are all marked
* @returns True if files are all marked
*/
_areFilesAllMarked() {
const filesForSimpleStaging = this.props.files.filter(file => !['unmerged', 'remote-changed'].includes(file.status));
return (filesForSimpleStaging.length !== 0 &&
filesForSimpleStaging.every(file => this.state.markedFiles.some(mf => areFilesEqual(file, mf))));
}
}