UNPKG

@jupyterlab/git

Version:

A JupyterLab extension for version control using git

781 lines (780 loc) 38.1 kB
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 { data, index, style } = rowProps; const file = data[index]; const diffButton = this._createDiffButton(file); 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, onDoubleClick: () => this._openDiffViews([file]), 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 doubleClickDiff = this.props.settings.get('doubleClickDiff') .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, onDoubleClick: doubleClickDiff ? diffButton ? () => this._openDiffViews([file]) : () => undefined : () => this.openSelectedFiles(file), 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 doubleClickDiff = this.props.settings.get('doubleClickDiff') .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, onDoubleClick: doubleClickDiff ? diffButton ? () => this._openDiffViews([file]) : () => undefined : () => this.openSelectedFiles(file), 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 doubleClickDiff = this.props.settings.get('doubleClickDiff') .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, onDoubleClick: () => { if (!doubleClickDiff) { this.props.commands.execute(ContextCommandIDs.gitFileOpen, { files: [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 doubleClickDiff = this.props.settings.get('doubleClickDiff') .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, onDoubleClick: () => { if (!doubleClickDiff) { this.props.commands.execute(ContextCommandIDs.gitFileOpen, { files: [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 doubleClickDiff = this.props.settings.get('doubleClickDiff') .composite; 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 = doubleClickDiff ? () => 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 = doubleClickDiff ? 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 = doubleClickDiff ? 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) }))); } /** * 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)))); } }