From bd822b73490c14dd34c08b1adfedc8f90b0a172c Mon Sep 17 00:00:00 2001 From: Navinn Ravindaran Date: Tue, 13 Jul 2021 19:03:43 -0400 Subject: [PATCH 01/10] Add history context menu button in the file browser (#863) --- src/commandsAndMenu.tsx | 30 ++++++++++++++++++++---------- src/components/FileList.tsx | 6 ++++-- src/model.ts | 28 ++++++++++++++++++++++++---- src/style/icons.ts | 5 +++++ src/tokens.ts | 9 ++++++++- src/widgets/GitWidget.tsx | 1 + style/icons/clock.svg | 14 ++++++++++++++ 7 files changed, 76 insertions(+), 17 deletions(-) create mode 100644 style/icons/clock.svg diff --git a/src/commandsAndMenu.tsx b/src/commandsAndMenu.tsx index 72ac411aa..5a758998e 100644 --- a/src/commandsAndMenu.tsx +++ b/src/commandsAndMenu.tsx @@ -34,7 +34,8 @@ import { discardIcon, gitIcon, openIcon, - removeIcon + removeIcon, + historyIcon } from './style/icons'; import { CommandIDs, @@ -897,6 +898,21 @@ export function addCommands( } }); + commands.addCommand(ContextCommandIDs.gitFileHistory, { + label: trans.__('History'), + caption: trans.__('View the history of this file'), + execute: args => { + const { + files: [file] + } = args as any as CommandArguments.IGitContextAction; + if (file && file.status === 'unmodified') { + gitModel.selectedHistoryFile = file.to; + shell.activateById('jp-git-sessions'); + } + }, + icon: historyIcon.bindprops({ stylesheet: 'menuItem' }) + }); + commands.addCommand(ContextCommandIDs.gitNoAction, { label: trans.__('No actions available'), isEnabled: () => false, @@ -1051,9 +1067,7 @@ export function addFileBrowserContextMenu( const items = getSelectedBrowserItems(); const statuses = new Set( - items - .map(item => model.getFile(item.path)?.status) - .filter(status => typeof status !== 'undefined') + items.map(item => model.getFile(item.path).status) ); // get commands and de-duplicate them @@ -1101,14 +1115,10 @@ export function addFileBrowserContextMenu( addMenuItems( commandsList, this, - paths - .map(path => model.getFile(path)) - // if file cannot be resolved (has no action available), - // omit the undefined result - .filter(file => typeof file !== 'undefined') + paths.map(path => model.getFile(path)) ); if (wasShown) { - // show he menu again after downtime for refresh + // show the menu again after downtime for refresh parent.triggerActiveItem(); } this._commands = commandsList; diff --git a/src/components/FileList.tsx b/src/components/FileList.tsx index 506ae2836..14d9c2d78 100644 --- a/src/components/FileList.tsx +++ b/src/components/FileList.tsx @@ -75,7 +75,8 @@ export const CONTEXT_COMMANDS: ContextCommands = { ContextCommandIDs.gitFileUnstage, ContextCommandIDs.gitFileDiff, ContextCommandIDs.gitCommitAmendStaged - ] + ], + unmodified: [ContextCommandIDs.gitFileHistory] }; const SIMPLE_CONTEXT_COMMANDS: ContextCommands = { @@ -99,7 +100,8 @@ const SIMPLE_CONTEXT_COMMANDS: ContextCommands = { ContextCommandIDs.gitIgnore, ContextCommandIDs.gitIgnoreExtension, ContextCommandIDs.gitFileDelete - ] + ], + unmodified: [ContextCommandIDs.gitFileHistory] }; export class FileList extends React.Component { diff --git a/src/model.ts b/src/model.ts index 6a8b52dc0..d1a3f74d4 100644 --- a/src/model.ts +++ b/src/model.ts @@ -186,6 +186,16 @@ export class GitExtension implements IGitExtension { this._standbyCondition = v; } + /** + * Selected file for single file history + */ + get selectedHistoryFile(): string { + return this._selectedHistoryFile; + } + set selectedHistoryFile(file: string) { + this._selectedHistoryFile = file; + } + /** * Git repository status */ @@ -271,14 +281,23 @@ export class GitExtension implements IGitExtension { /** * Match files status information based on a provided file path. * - * If the file is tracked and has no changes, undefined will be returned + * If the file is tracked and has no changes, a StatusFile of unmodified will be returned * * @param path the file path relative to the server root */ getFile(path: string): Git.IStatusFile { - return this._status.files.find(status => { - return this.getRelativeFilePath(status.to) === path; - }); + return ( + this._status.files.find(status => { + return this.getRelativeFilePath(status.to) === path; + }) ?? { + x: '', + y: '', + to: path, + from: '', + is_binary: null, + status: 'unmodified' + } + ); } /** @@ -1375,6 +1394,7 @@ export class GitExtension implements IGitExtension { private _standbyCondition: () => boolean = () => false; private _statusPoll: Poll; private _taskHandler: TaskHandler; + private _selectedHistoryFile: string = ''; private _headChanged = new Signal(this); private _markChanged = new Signal(this); diff --git a/src/style/icons.ts b/src/style/icons.ts index fd5e8191c..b93b74e77 100644 --- a/src/style/icons.ts +++ b/src/style/icons.ts @@ -17,6 +17,7 @@ import removeSvg from '../../style/icons/remove.svg'; import rewindSvg from '../../style/icons/rewind.svg'; import tagSvg from '../../style/icons/tag.svg'; import trashSvg from '../../style/icons/trash.svg'; +import clockSvg from '../../style/icons/clock.svg'; export const gitIcon = new LabIcon({ name: 'git', svgstr: gitSvg }); export const addIcon = new LabIcon({ @@ -79,3 +80,7 @@ export const trashIcon = new LabIcon({ name: 'git:trash', svgstr: trashSvg }); +export const historyIcon = new LabIcon({ + name: 'git:history', + svgstr: clockSvg +}); diff --git a/src/tokens.ts b/src/tokens.ts index 52d808a15..bc6eb82ef 100644 --- a/src/tokens.ts +++ b/src/tokens.ts @@ -55,6 +55,11 @@ export interface IGitExtension extends IDisposable { */ refreshStandbyCondition: () => boolean; + /** + * Selected file for single file history + */ + selectedHistoryFile: string; + /** * Git repository status. */ @@ -253,7 +258,7 @@ export interface IGitExtension extends IDisposable { /** * Match files status information based on a provided file path. * - * If the file is tracked and has no changes, undefined will be returned + * If the file is tracked and has no changes, a StatusFile of unmodified will be returned * * @param path the file path relative to the server root */ @@ -863,6 +868,7 @@ export namespace Git { | 'staged' | 'unstaged' | 'partially-staged' + | 'unmodified' | null; export interface ITagResult { @@ -959,6 +965,7 @@ export enum ContextCommandIDs { gitFileUnstage = 'git:context-unstage', gitFileStage = 'git:context-stage', gitFileTrack = 'git:context-track', + gitFileHistory = 'git:context-history', gitIgnore = 'git:context-ignore', gitIgnoreExtension = 'git:context-ignoreExtension', gitNoAction = 'git:no-action' diff --git a/src/widgets/GitWidget.tsx b/src/widgets/GitWidget.tsx index 1e1e1bb9c..caf399645 100644 --- a/src/widgets/GitWidget.tsx +++ b/src/widgets/GitWidget.tsx @@ -47,6 +47,7 @@ export class GitWidget extends ReactWidget { * The default implementation of this handler is a no-op. */ onBeforeShow(msg: Message): void { + // TODO: Do something here with the model @fcollonval // Trigger refresh when the widget is displayed this._model.refresh().catch(error => { console.error('Fail to refresh model when displaying GitWidget.', error); diff --git a/style/icons/clock.svg b/style/icons/clock.svg new file mode 100644 index 000000000..b46649488 --- /dev/null +++ b/style/icons/clock.svg @@ -0,0 +1,14 @@ + From 0808588af7e0ee8dc832c3e3ada9b93a109efc37 Mon Sep 17 00:00:00 2001 From: Navinn Ravindaran Date: Tue, 20 Jul 2021 11:38:23 -0400 Subject: [PATCH 02/10] Add --follow option to Git log command (#864) --- jupyterlab_git/git.py | 5 ++++- jupyterlab_git/handlers.py | 7 +++++-- src/model.ts | 3 ++- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/jupyterlab_git/git.py b/jupyterlab_git/git.py index 8a002b549..98494103a 100644 --- a/jupyterlab_git/git.py +++ b/jupyterlab_git/git.py @@ -432,7 +432,7 @@ async def status(self, path): return data - async def log(self, path, history_count=10): + async def log(self, path, history_count=10, follow_path="."): """ Execute git log command & return the result. """ @@ -441,6 +441,9 @@ async def log(self, path, history_count=10): "log", "--pretty=format:%H%n%an%n%ar%n%s", ("-%d" % history_count), + "--follow", + "--", + follow_path, ] code, my_output, my_error = await execute( cmd, diff --git a/jupyterlab_git/handlers.py b/jupyterlab_git/handlers.py index 076f89da1..d4fa6a3cc 100644 --- a/jupyterlab_git/handlers.py +++ b/jupyterlab_git/handlers.py @@ -196,7 +196,7 @@ async def post(self, path: str = ""): class GitLogHandler(GitHandler): """ - Handler for 'git log --pretty=format:%H-%an-%ar-%s'. + Handler for 'git log --pretty=format:%H-%an-%ar-%s --follow --'. Fetches Commit SHA, Author Name, Commit Date & Commit Message. """ @@ -208,7 +208,10 @@ async def post(self, path: str = ""): """ body = self.get_json_body() history_count = body.get("history_count", 25) - result = await self.git.log(self.url2localpath(path), history_count) + follow_path = body.get("follow_path", ".") + result = await self.git.log( + self.url2localpath(path), history_count, follow_path + ) if result["code"] != 0: self.set_status(500) diff --git a/src/model.ts b/src/model.ts index d1a3f74d4..329e03f2c 100644 --- a/src/model.ts +++ b/src/model.ts @@ -711,7 +711,8 @@ export class GitExtension implements IGitExtension { URLExt.join(path, 'log'), 'POST', { - history_count: count + history_count: count, + follow_path: this.selectedHistoryFile || '.' } ); } From e3adc3f1a1bd8846f10d74f283e3603bad718737 Mon Sep 17 00:00:00 2001 From: Navinn Ravindaran Date: Tue, 20 Jul 2021 18:50:23 -0400 Subject: [PATCH 03/10] Display Git history for selected file (#864) --- jupyterlab_git/git.py | 43 ++++---- src/commandsAndMenu.tsx | 2 +- src/components/FileItem.tsx | 2 +- src/components/GitPanel.tsx | 4 + src/components/HistorySideBar.tsx | 129 +++++++++++++++++++++--- src/components/PastCommitNode.tsx | 61 +++++++---- src/components/SinglePastCommitInfo.tsx | 73 +++----------- src/model.ts | 28 ++++- src/style/PastCommitNode.ts | 8 ++ src/tokens.ts | 11 +- src/widgets/GitWidget.tsx | 1 - 11 files changed, 236 insertions(+), 126 deletions(-) diff --git a/jupyterlab_git/git.py b/jupyterlab_git/git.py index 98494103a..ea227241e 100644 --- a/jupyterlab_git/git.py +++ b/jupyterlab_git/git.py @@ -436,15 +436,15 @@ async def log(self, path, history_count=10, follow_path="."): """ Execute git log command & return the result. """ + is_single_file = follow_path != "." cmd = [ "git", "log", "--pretty=format:%H%n%an%n%ar%n%s", ("-%d" % history_count), - "--follow", - "--", - follow_path, ] + if is_single_file: + cmd = cmd + ["--numstat", "-z", "--follow", "--", follow_path] code, my_output, my_error = await execute( cmd, cwd=path, @@ -453,30 +453,25 @@ async def log(self, path, history_count=10, follow_path="."): return {"code": code, "command": " ".join(cmd), "message": my_error} result = [] - line_array = my_output.splitlines() + line_array = my_output.replace("\0\0", "\n").splitlines() i = 0 - PREVIOUS_COMMIT_OFFSET = 4 + PREVIOUS_COMMIT_OFFSET = 5 if is_single_file else 4 while i < len(line_array): + commit = { + "commit": line_array[i], + "author": line_array[i + 1], + "date": line_array[i + 2], + "commit_msg": line_array[i + 3], + "pre_commit": "", + } + + if is_single_file: + commit["is_binary"] = line_array[i + 4].startswith("-\t-\t") + if i + PREVIOUS_COMMIT_OFFSET < len(line_array): - result.append( - { - "commit": line_array[i], - "author": line_array[i + 1], - "date": line_array[i + 2], - "commit_msg": line_array[i + 3], - "pre_commit": line_array[i + PREVIOUS_COMMIT_OFFSET], - } - ) - else: - result.append( - { - "commit": line_array[i], - "author": line_array[i + 1], - "date": line_array[i + 2], - "commit_msg": line_array[i + 3], - "pre_commit": "", - } - ) + commit["pre_commit"] = line_array[i + PREVIOUS_COMMIT_OFFSET] + + result.append(commit) i += PREVIOUS_COMMIT_OFFSET return {"code": code, "commits": result} diff --git a/src/commandsAndMenu.tsx b/src/commandsAndMenu.tsx index 5a758998e..34a4f6d26 100644 --- a/src/commandsAndMenu.tsx +++ b/src/commandsAndMenu.tsx @@ -906,7 +906,7 @@ export function addCommands( files: [file] } = args as any as CommandArguments.IGitContextAction; if (file && file.status === 'unmodified') { - gitModel.selectedHistoryFile = file.to; + gitModel.selectedHistoryFile = file; shell.activateById('jp-git-sessions'); } }, diff --git a/src/components/FileItem.tsx b/src/components/FileItem.tsx index eb0cd74f2..e216198a9 100644 --- a/src/components/FileItem.tsx +++ b/src/components/FileItem.tsx @@ -133,7 +133,7 @@ export class FileItem extends React.PureComponent { super(props); } protected _getFileChangedLabel(change: keyof typeof STATUS_CODES): string { - return STATUS_CODES[change]; + return STATUS_CODES[change] || 'Unmodified'; } protected _getFileChangedLabelClass(change: string): string { diff --git a/src/components/GitPanel.tsx b/src/components/GitPanel.tsx index a7ffd855e..fe7b4ffa5 100644 --- a/src/components/GitPanel.tsx +++ b/src/components/GitPanel.tsx @@ -176,6 +176,10 @@ export class GitPanel extends React.Component { this.refreshHistory(); } }, this); + model.selectedHistoryFileChanged.connect(() => { + this.setState({ tab: 1 }); + this.refreshHistory(); + }, this); model.markChanged.connect(() => this.forceUpdate(), this); settings.changed.connect(this.refreshView, this); diff --git a/src/components/HistorySideBar.tsx b/src/components/HistorySideBar.tsx index fefad6efa..6557c1c9e 100644 --- a/src/components/HistorySideBar.tsx +++ b/src/components/HistorySideBar.tsx @@ -1,10 +1,16 @@ import { TranslationBundle } from '@jupyterlab/translation'; import { CommandRegistry } from '@lumino/commands'; import * as React from 'react'; +import { CommandArguments } from '../commandsAndMenu'; import { GitExtension } from '../model'; +import { hiddenButtonStyle } from '../style/ActionButtonStyle'; import { historySideBarStyle } from '../style/HistorySideBarStyle'; -import { Git } from '../tokens'; +import { removeIcon } from '../style/icons'; +import { ContextCommandIDs, Git } from '../tokens'; +import { ActionButton } from './ActionButton'; +import { FileItem } from './FileItem'; import { PastCommitNode } from './PastCommitNode'; +import { SinglePastCommitInfo } from './SinglePastCommitInfo'; /** * Interface describing component properties. @@ -44,17 +50,110 @@ export interface IHistorySideBarProps { */ export const HistorySideBar: React.FunctionComponent = ( props: IHistorySideBarProps -): React.ReactElement => ( -
    - {props.commits.map((commit: Git.ISingleCommitInfo) => ( - - ))} -
-); +): React.ReactElement => { + /** + * Discards the selected file and shows the full history. + */ + const removeSelectedFile = () => { + props.model.selectedHistoryFile = null; + }; + + /** + * Curried callback function to display a file diff. + * + * @param commit Commit data. + */ + const openDiff = + (commit: Git.ISingleCommitInfo) => + /** + * Returns a callback to be invoked on click to display a file diff. + * + * @param filePath file path + * @param isText indicates whether the file supports displaying a diff + * @returns callback + */ + (filePath: string, isText: boolean) => + /** + * Callback invoked upon clicking to display a file diff. + * + * @param event - event object + */ + async (event: React.MouseEvent) => { + // Prevent the commit component from being collapsed: + event.stopPropagation(); + + if (isText) { + try { + props.commands.execute(ContextCommandIDs.gitFileDiff, { + files: [ + { + filePath, + isText, + context: { + previousRef: commit.pre_commit, + currentRef: commit.commit + } + } + ] + } as CommandArguments.IGitFileDiff as any); + } catch (err) { + console.error(`Failed to open diff view for ${filePath}.\n${err}`); + } + } + }; + + return ( +
    + {!!props.model.selectedHistoryFile && ( + + } + file={props.model.selectedHistoryFile} + onDoubleClick={removeSelectedFile} + /> + )} + {props.commits.map((commit: Git.ISingleCommitInfo) => { + const commitNodeProps = { + commit, + branches: props.branches, + model: props.model, + commands: props.commands, + trans: props.trans + }; + + // Only pass down callback when single file history is open and diff is viewable + const onOpenDiff = + props.model.selectedHistoryFile && !commit.is_binary + ? openDiff(commit)( + props.model.selectedHistoryFile.to, + !commit.is_binary + ) + : undefined; + + return ( + + {!props.model.selectedHistoryFile && ( + + )} + + ); + })} +
+ ); +}; diff --git a/src/components/PastCommitNode.tsx b/src/components/PastCommitNode.tsx index 1ca7ea298..bd1dec9bb 100644 --- a/src/components/PastCommitNode.tsx +++ b/src/components/PastCommitNode.tsx @@ -4,6 +4,7 @@ import { CommandRegistry } from '@lumino/commands'; import * as React from 'react'; import { classes } from 'typestyle'; import { GitExtension } from '../model'; +import { diffIcon } from '../style/icons'; import { branchClass, branchWrapperClass, @@ -15,10 +16,11 @@ import { iconButtonClass, localBranchClass, remoteBranchClass, + singleFileCommitClass, workingBranchClass } from '../style/PastCommitNode'; import { Git } from '../tokens'; -import { SinglePastCommitInfo } from './SinglePastCommitInfo'; +import { ActionButton } from './ActionButton'; /** * Interface describing component properties. @@ -48,6 +50,15 @@ export interface IPastCommitNodeProps { * The application language translator. */ trans: TranslationBundle; + + /** + * Callback invoked upon clicking to display a file diff. + * + * @param event - event object + */ + onOpenDiff?: ( + event: React.MouseEvent + ) => Promise; } /** @@ -64,7 +75,7 @@ export interface IPastCommitNodeState { * React component for rendering an individual commit. */ export class PastCommitNode extends React.Component< - IPastCommitNodeProps, + React.PropsWithChildren, IPastCommitNodeState > { /** @@ -73,7 +84,7 @@ export class PastCommitNode extends React.Component< * @param props - component properties * @returns React component */ - constructor(props: IPastCommitNodeProps) { + constructor(props: React.PropsWithChildren) { super(props); this.state = { expanded: false @@ -90,7 +101,11 @@ export class PastCommitNode extends React.Component<
  • @@ -104,23 +119,25 @@ export class PastCommitNode extends React.Component< {this.props.commit.date} - {this.state.expanded ? ( - + {this.props.children ? ( + this.state.expanded ? ( + + ) : ( + + ) ) : ( - + !!this.props.onOpenDiff && ( + + ) )}
    {this._renderBranches()}
    {this.props.commit.commit_msg} - {this.state.expanded && ( - - )} + {this.state.expanded && this.props.children}
  • ); @@ -174,9 +191,15 @@ export class PastCommitNode extends React.Component< * * @param event - event object */ - private _onCommitClick = (): void => { - this.setState({ - expanded: !this.state.expanded - }); + private _onCommitClick = ( + event: React.MouseEvent + ): void => { + if (this.props.children) { + this.setState({ + expanded: !this.state.expanded + }); + } else if (!!this.props.onOpenDiff) { + this.props.onOpenDiff(event); + } }; } diff --git a/src/components/SinglePastCommitInfo.tsx b/src/components/SinglePastCommitInfo.tsx index 8096ff62a..3cf189c04 100644 --- a/src/components/SinglePastCommitInfo.tsx +++ b/src/components/SinglePastCommitInfo.tsx @@ -4,7 +4,6 @@ import { CommandRegistry } from '@lumino/commands'; import * as React from 'react'; import { FixedSizeList, ListChildComponentProps } from 'react-window'; import { classes } from 'typestyle'; -import { CommandArguments } from '../commandsAndMenu'; import { LoggerContext } from '../logger'; import { getDiffProvider, GitExtension } from '../model'; import { @@ -26,7 +25,7 @@ import { iconClass, insertionsIconClass } from '../style/SinglePastCommitInfo'; -import { ContextCommandIDs, Git } from '../tokens'; +import { Git } from '../tokens'; import { ActionButton } from './ActionButton'; import { FilePath } from './FilePath'; import { ResetRevertDialog } from './ResetRevertDialog'; @@ -57,6 +56,18 @@ export interface ISinglePastCommitInfoProps { * The application language translator. */ trans: TranslationBundle; + + /** + * Returns a callback to be invoked on click to display a file diff. + * + * @param filePath file path + * @param isText indicates whether the file supports displaying a diff + * @returns callback + */ + onOpenDiff: ( + filePath: string, + isText: boolean + ) => (event: React.MouseEvent) => void; } /** @@ -262,7 +273,7 @@ export class SinglePastCommitInfo extends React.Component< return (
  • @@ -311,60 +322,4 @@ export class SinglePastCommitInfo extends React.Component< resetRevertDialog: false }); }; - - /** - * Returns a callback to be invoked clicking a button to display a file diff. - * - * @param fpath - modified file path - * @param bool - boolean indicating whether a displaying a diff is supported for this file path - * @returns callback - */ - private _onDiffClickFactory(fpath: string, bool: boolean) { - const self = this; - if (bool) { - return onShowDiff; - } - return onClick; - - /** - * Callback invoked upon clicking a button to display a file diff. - * - * @private - * @param event - event object - */ - function onClick(event: React.MouseEvent) { - // Prevent the commit component from being collapsed: - event.stopPropagation(); - } - - /** - * Callback invoked upon clicking a button to display a file diff. - * - * @private - * @param event - event object - */ - async function onShowDiff( - event: React.MouseEvent - ) { - // Prevent the commit component from being collapsed: - event.stopPropagation(); - - try { - self.props.commands.execute(ContextCommandIDs.gitFileDiff, { - files: [ - { - filePath: fpath, - isText: bool, - context: { - previousRef: self.props.commit.pre_commit, - currentRef: self.props.commit.commit - } - } - ] - } as CommandArguments.IGitFileDiff as any); - } catch (err) { - console.error(`Failed to open diff view for ${fpath}.\n${err}`); - } - } - } } diff --git a/src/model.ts b/src/model.ts index 329e03f2c..ab5b2a748 100644 --- a/src/model.ts +++ b/src/model.ts @@ -189,11 +189,12 @@ export class GitExtension implements IGitExtension { /** * Selected file for single file history */ - get selectedHistoryFile(): string { + get selectedHistoryFile(): Git.IStatusFile | null { return this._selectedHistoryFile; } - set selectedHistoryFile(file: string) { + set selectedHistoryFile(file: Git.IStatusFile | null) { this._selectedHistoryFile = file; + this._selectedHistoryFileChanged.emit(file); } /** @@ -217,6 +218,16 @@ export class GitExtension implements IGitExtension { return this._markChanged; } + /** + * A signal emitted when the current file selected for history of the Git repository changes. + */ + get selectedHistoryFileChanged(): ISignal< + IGitExtension, + Git.IStatusFile | null + > { + return this._selectedHistoryFileChanged; + } + /** * A signal emitted when the current Git repository changes. */ @@ -295,7 +306,8 @@ export class GitExtension implements IGitExtension { to: path, from: '', is_binary: null, - status: 'unmodified' + status: 'unmodified', + type: {} as DocumentRegistry.IFileType } ); } @@ -712,7 +724,9 @@ export class GitExtension implements IGitExtension { 'POST', { history_count: count, - follow_path: this.selectedHistoryFile || '.' + follow_path: !!this.selectedHistoryFile + ? this.selectedHistoryFile.to + : '.' } ); } @@ -1395,10 +1409,14 @@ export class GitExtension implements IGitExtension { private _standbyCondition: () => boolean = () => false; private _statusPoll: Poll; private _taskHandler: TaskHandler; - private _selectedHistoryFile: string = ''; + private _selectedHistoryFile: Git.IStatusFile | null = null; private _headChanged = new Signal(this); private _markChanged = new Signal(this); + private _selectedHistoryFileChanged = new Signal< + IGitExtension, + Git.IStatusFile | null + >(this); private _repositoryChanged = new Signal< IGitExtension, IChangedArgs diff --git a/src/style/PastCommitNode.ts b/src/style/PastCommitNode.ts index 9bd625b7c..33cea04a1 100644 --- a/src/style/PastCommitNode.ts +++ b/src/style/PastCommitNode.ts @@ -75,3 +75,11 @@ export const iconButtonClass = style({ /* top | right | bottom | left */ margin: 'auto 8px auto auto' }); + +export const singleFileCommitClass = style({ + $nest: { + '&:hover': { + backgroundColor: 'var(--jp-layout-color2)' + } + } +}); diff --git a/src/tokens.ts b/src/tokens.ts index bc6eb82ef..1752d3fe7 100644 --- a/src/tokens.ts +++ b/src/tokens.ts @@ -58,7 +58,7 @@ export interface IGitExtension extends IDisposable { /** * Selected file for single file history */ - selectedHistoryFile: string; + selectedHistoryFile: Git.IStatusFile | null; /** * Git repository status. @@ -75,6 +75,14 @@ export interface IGitExtension extends IDisposable { */ readonly taskChanged: ISignal; + /** + * A signal emitted when the current file selected for history of the Git repository changes. + */ + readonly selectedHistoryFileChanged: ISignal< + IGitExtension, + Git.IStatusFile | null + >; + /** * Add one or more files to the repository staging area. * @@ -770,6 +778,7 @@ export namespace Git { date: string; commit_msg: string; pre_commit: string; + is_binary?: boolean; // for single file history } /** Interface for GitCommit request result, diff --git a/src/widgets/GitWidget.tsx b/src/widgets/GitWidget.tsx index caf399645..1e1e1bb9c 100644 --- a/src/widgets/GitWidget.tsx +++ b/src/widgets/GitWidget.tsx @@ -47,7 +47,6 @@ export class GitWidget extends ReactWidget { * The default implementation of this handler is a no-op. */ onBeforeShow(msg: Message): void { - // TODO: Do something here with the model @fcollonval // Trigger refresh when the widget is displayed this._model.refresh().catch(error => { console.error('Fail to refresh model when displaying GitWidget.', error); From 26d257162ab0f35c10fdb282e2a3b5efaeee2be0 Mon Sep 17 00:00:00 2001 From: Navinn Ravindaran Date: Wed, 21 Jul 2021 12:13:34 -0400 Subject: [PATCH 04/10] Apply suggestions from code review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Frédéric Collonval --- jupyterlab_git/git.py | 16 ++++++++++++---- jupyterlab_git/handlers.py | 4 ++-- src/commandsAndMenu.tsx | 18 +++++++++++------- src/components/HistorySideBar.tsx | 4 ++-- src/components/PastCommitNode.tsx | 26 ++++++++++---------------- src/model.ts | 10 +++++----- style/icons/clock.svg | 3 +-- 7 files changed, 43 insertions(+), 38 deletions(-) diff --git a/jupyterlab_git/git.py b/jupyterlab_git/git.py index ea227241e..2cd15b041 100644 --- a/jupyterlab_git/git.py +++ b/jupyterlab_git/git.py @@ -432,11 +432,11 @@ async def status(self, path): return data - async def log(self, path, history_count=10, follow_path="."): + async def log(self, path, history_count=10, follow_path=None): """ Execute git log command & return the result. """ - is_single_file = follow_path != "." + is_single_file = follow_path != None cmd = [ "git", "log", @@ -444,7 +444,12 @@ async def log(self, path, history_count=10, follow_path="."): ("-%d" % history_count), ] if is_single_file: - cmd = cmd + ["--numstat", "-z", "--follow", "--", follow_path] + cmd = cmd + [ + "--numstat", + "--follow", + "--", + follow_path, + ] code, my_output, my_error = await execute( cmd, cwd=path, @@ -453,7 +458,10 @@ async def log(self, path, history_count=10, follow_path="."): return {"code": code, "command": " ".join(cmd), "message": my_error} result = [] - line_array = my_output.replace("\0\0", "\n").splitlines() + if is_single_file: + # an extra newline get outputted when --numstat is used + my_output = my_output.replace("\n\n", "\n") + line_array = my_output.splitlines() i = 0 PREVIOUS_COMMIT_OFFSET = 5 if is_single_file else 4 while i < len(line_array): diff --git a/jupyterlab_git/handlers.py b/jupyterlab_git/handlers.py index d4fa6a3cc..6cb61a179 100644 --- a/jupyterlab_git/handlers.py +++ b/jupyterlab_git/handlers.py @@ -196,7 +196,7 @@ async def post(self, path: str = ""): class GitLogHandler(GitHandler): """ - Handler for 'git log --pretty=format:%H-%an-%ar-%s --follow --'. + Handler for 'git log'. Fetches Commit SHA, Author Name, Commit Date & Commit Message. """ @@ -208,7 +208,7 @@ async def post(self, path: str = ""): """ body = self.get_json_body() history_count = body.get("history_count", 25) - follow_path = body.get("follow_path", ".") + follow_path = body.get("follow_path") result = await self.git.log( self.url2localpath(path), history_count, follow_path ) diff --git a/src/commandsAndMenu.tsx b/src/commandsAndMenu.tsx index 34a4f6d26..4a4f2440c 100644 --- a/src/commandsAndMenu.tsx +++ b/src/commandsAndMenu.tsx @@ -902,14 +902,19 @@ export function addCommands( label: trans.__('History'), caption: trans.__('View the history of this file'), execute: args => { - const { - files: [file] - } = args as any as CommandArguments.IGitContextAction; - if (file && file.status === 'unmodified') { + const { files } = args as any as CommandArguments.IGitContextAction; + const file = files[0]; + if (!file) { + return; + } else if (file.status === 'unmodified') { gitModel.selectedHistoryFile = file; shell.activateById('jp-git-sessions'); } }, + isEnabled: args => { + const { files } = args as any as CommandArguments.IGitContextAction; + return files.length === 1; + }, icon: historyIcon.bindprops({ stylesheet: 'menuItem' }) }); @@ -1092,10 +1097,9 @@ export function addFileBrowserContextMenu( ) ); - // if looking at a tracked file with no changes, - // it has no status, nor any actions available + // if looking at a tracked file without any actions available // (although `git rm` would be a valid action) - if (allCommands.size === 0 && statuses.size === 0) { + if (allCommands.size === 0) { allCommands.add(ContextCommandIDs.gitNoAction); } diff --git a/src/components/HistorySideBar.tsx b/src/components/HistorySideBar.tsx index 6557c1c9e..0ab649f42 100644 --- a/src/components/HistorySideBar.tsx +++ b/src/components/HistorySideBar.tsx @@ -1,11 +1,11 @@ import { TranslationBundle } from '@jupyterlab/translation'; +import { closeIcon } from '@jupyterlab/ui-components'; import { CommandRegistry } from '@lumino/commands'; import * as React from 'react'; import { CommandArguments } from '../commandsAndMenu'; import { GitExtension } from '../model'; import { hiddenButtonStyle } from '../style/ActionButtonStyle'; import { historySideBarStyle } from '../style/HistorySideBarStyle'; -import { removeIcon } from '../style/icons'; import { ContextCommandIDs, Git } from '../tokens'; import { ActionButton } from './ActionButton'; import { FileItem } from './FileItem'; @@ -112,7 +112,7 @@ export const HistorySideBar: React.FunctionComponent = ( actions={ diff --git a/src/components/PastCommitNode.tsx b/src/components/PastCommitNode.tsx index bd1dec9bb..86036879f 100644 --- a/src/components/PastCommitNode.tsx +++ b/src/components/PastCommitNode.tsx @@ -4,7 +4,6 @@ import { CommandRegistry } from '@lumino/commands'; import * as React from 'react'; import { classes } from 'typestyle'; import { GitExtension } from '../model'; -import { diffIcon } from '../style/icons'; import { branchClass, branchWrapperClass, @@ -20,7 +19,6 @@ import { workingBranchClass } from '../style/PastCommitNode'; import { Git } from '../tokens'; -import { ActionButton } from './ActionButton'; /** * Interface describing component properties. @@ -107,6 +105,11 @@ export class PastCommitNode extends React.Component< ? commitExpandedClass : null )} + title={ + this.props.children + ? this.props.trans.__('View commit details') + : this.props.trans.__('View file changes') + } onClick={this._onCommitClick} >
    @@ -119,19 +122,10 @@ export class PastCommitNode extends React.Component< {this.props.commit.date} - {this.props.children ? ( - this.state.expanded ? ( - - ) : ( - - ) + {this.props.children && this.state.expanded ? ( + ) : ( - !!this.props.onOpenDiff && ( - - ) + )}
    {this._renderBranches()}
    @@ -198,8 +192,8 @@ export class PastCommitNode extends React.Component< this.setState({ expanded: !this.state.expanded }); - } else if (!!this.props.onOpenDiff) { - this.props.onOpenDiff(event); + } else { + this.props.onOpenDiff?.call(this, event); } }; } diff --git a/src/model.ts b/src/model.ts index ab5b2a748..4c5d8b185 100644 --- a/src/model.ts +++ b/src/model.ts @@ -193,8 +193,10 @@ export class GitExtension implements IGitExtension { return this._selectedHistoryFile; } set selectedHistoryFile(file: Git.IStatusFile | null) { - this._selectedHistoryFile = file; - this._selectedHistoryFileChanged.emit(file); + if (this._selectedHistoryFile !== file) { + this._selectedHistoryFile = file; + this._selectedHistoryFileChanged.emit(file); + } } /** @@ -724,9 +726,7 @@ export class GitExtension implements IGitExtension { 'POST', { history_count: count, - follow_path: !!this.selectedHistoryFile - ? this.selectedHistoryFile.to - : '.' + follow_path: this.selectedHistoryFile?.to } ); } diff --git a/style/icons/clock.svg b/style/icons/clock.svg index b46649488..8b3c96162 100644 --- a/style/icons/clock.svg +++ b/style/icons/clock.svg @@ -1,13 +1,12 @@
      {!!props.model.selectedHistoryFile && ( {this.props.commit.date} - {this.props.children && this.state.expanded ? ( - + {this.props.children ? ( + this.state.expanded ? ( + + ) : ( + + ) ) : ( - + )}
      {this._renderBranches()}
      diff --git a/src/model.ts b/src/model.ts index 4c5d8b185..50ba00e81 100644 --- a/src/model.ts +++ b/src/model.ts @@ -309,7 +309,7 @@ export class GitExtension implements IGitExtension { from: '', is_binary: null, status: 'unmodified', - type: {} as DocumentRegistry.IFileType + type: this._resolveFileType(path) } ); } diff --git a/src/style/HistorySideBarStyle.ts b/src/style/HistorySideBarStyle.ts index 7702fa78b..17da461dc 100644 --- a/src/style/HistorySideBarStyle.ts +++ b/src/style/HistorySideBarStyle.ts @@ -1,5 +1,19 @@ import { style } from 'typestyle'; +export const selectedHistoryFileStyle = style({ + minHeight: '48px', + + top: 0, + position: 'sticky', + + flexGrow: 0, + flexShrink: 0, + + overflowX: 'hidden', + + backgroundColor: 'var(--jp-toolbar-active-background)' +}); + export const historySideBarStyle = style({ display: 'flex', flexDirection: 'column', From 237f2c0cffc3144d45dcbd87c059852570e4e2d4 Mon Sep 17 00:00:00 2001 From: Navinn Ravindaran Date: Sat, 24 Jul 2021 18:52:37 -0400 Subject: [PATCH 06/10] Add selected file history for modified files --- src/commandsAndMenu.tsx | 5 ++--- src/components/FileList.tsx | 18 +++++++++++------ src/components/HistorySideBar.tsx | 32 ++++++++++++++++++++++++++----- src/components/PastCommitNode.tsx | 4 +++- 4 files changed, 44 insertions(+), 15 deletions(-) diff --git a/src/commandsAndMenu.tsx b/src/commandsAndMenu.tsx index 4a4f2440c..4b5dfba8e 100644 --- a/src/commandsAndMenu.tsx +++ b/src/commandsAndMenu.tsx @@ -906,10 +906,9 @@ export function addCommands( const file = files[0]; if (!file) { return; - } else if (file.status === 'unmodified') { - gitModel.selectedHistoryFile = file; - shell.activateById('jp-git-sessions'); } + gitModel.selectedHistoryFile = file; + shell.activateById('jp-git-sessions'); }, isEnabled: args => { const { files } = args as any as CommandArguments.IGitContextAction; diff --git a/src/components/FileList.tsx b/src/components/FileList.tsx index 14d9c2d78..88a4b5bf8 100644 --- a/src/components/FileList.tsx +++ b/src/components/FileList.tsx @@ -55,13 +55,15 @@ export const CONTEXT_COMMANDS: ContextCommands = { 'partially-staged': [ ContextCommandIDs.gitFileOpen, ContextCommandIDs.gitFileUnstage, - ContextCommandIDs.gitFileDiff + ContextCommandIDs.gitFileDiff, + ContextCommandIDs.gitFileHistory ], unstaged: [ ContextCommandIDs.gitFileOpen, ContextCommandIDs.gitFileStage, ContextCommandIDs.gitFileDiscard, - ContextCommandIDs.gitFileDiff + ContextCommandIDs.gitFileDiff, + ContextCommandIDs.gitFileHistory ], untracked: [ ContextCommandIDs.gitFileOpen, @@ -74,7 +76,8 @@ export const CONTEXT_COMMANDS: ContextCommands = { ContextCommandIDs.gitFileOpen, ContextCommandIDs.gitFileUnstage, ContextCommandIDs.gitFileDiff, - ContextCommandIDs.gitCommitAmendStaged + ContextCommandIDs.gitCommitAmendStaged, + ContextCommandIDs.gitFileHistory ], unmodified: [ContextCommandIDs.gitFileHistory] }; @@ -83,17 +86,20 @@ const SIMPLE_CONTEXT_COMMANDS: ContextCommands = { 'partially-staged': [ ContextCommandIDs.gitFileOpen, ContextCommandIDs.gitFileDiscard, - ContextCommandIDs.gitFileDiff + ContextCommandIDs.gitFileDiff, + ContextCommandIDs.gitFileHistory ], staged: [ ContextCommandIDs.gitFileOpen, ContextCommandIDs.gitFileDiscard, - ContextCommandIDs.gitFileDiff + ContextCommandIDs.gitFileDiff, + ContextCommandIDs.gitFileHistory ], unstaged: [ ContextCommandIDs.gitFileOpen, ContextCommandIDs.gitFileDiscard, - ContextCommandIDs.gitFileDiff + ContextCommandIDs.gitFileDiff, + ContextCommandIDs.gitFileHistory ], untracked: [ ContextCommandIDs.gitFileOpen, diff --git a/src/components/HistorySideBar.tsx b/src/components/HistorySideBar.tsx index 410730062..e6b17e4b2 100644 --- a/src/components/HistorySideBar.tsx +++ b/src/components/HistorySideBar.tsx @@ -105,6 +105,27 @@ export const HistorySideBar: React.FunctionComponent = ( } }; + /** + * Commit info for 'Uncommitted Changes' history. + */ + const uncommitted = React.useMemo(() => { + return { + author: props.trans.__('You'), + // INDEX or WORKING special ref + commit: `${+(props.model.selectedHistoryFile?.status === 'staged')}`, + pre_commit: 'HEAD', + is_binary: !!props.commits[0]?.is_binary, + commit_msg: props.trans.__('Uncommitted Changes'), + date: props.trans.__('now') + }; + }, [props.model.selectedHistoryFile]); + + const commits = + props.model.selectedHistoryFile && + props.model.selectedHistoryFile?.status !== 'unmodified' + ? [uncommitted, ...props.commits] + : props.commits; + return (
        {!!props.model.selectedHistoryFile && ( @@ -124,8 +145,8 @@ export const HistorySideBar: React.FunctionComponent = ( onDoubleClick={removeSelectedFile} /> )} - {props.commits.map((commit: Git.ISingleCommitInfo) => { - const commitNodeProps = { + {commits.map((commit: Git.ISingleCommitInfo) => { + const commonProps = { commit, branches: props.branches, model: props.model, @@ -133,7 +154,8 @@ export const HistorySideBar: React.FunctionComponent = ( trans: props.trans }; - // Only pass down callback when single file history is open and diff is viewable + // Only pass down callback when single file history is open + // and its diff is viewable const onOpenDiff = props.model.selectedHistoryFile && !commit.is_binary ? openDiff(commit)( @@ -145,12 +167,12 @@ export const HistorySideBar: React.FunctionComponent = ( return ( {!props.model.selectedHistoryFile && ( )} diff --git a/src/components/PastCommitNode.tsx b/src/components/PastCommitNode.tsx index 7f7ff0df9..f9cb2eaa2 100644 --- a/src/components/PastCommitNode.tsx +++ b/src/components/PastCommitNode.tsx @@ -118,7 +118,9 @@ export class PastCommitNode extends React.Component< {this.props.commit.author} - {this.props.commit.commit.slice(0, 7)} + {+this.props.commit.commit in Git.Diff.SpecialRef + ? Git.Diff.SpecialRef[+this.props.commit.commit] + : this.props.commit.commit.slice(0, 7)} {this.props.commit.date} From f5292c5239e839cff567e17cb7af17fb7d022f49 Mon Sep 17 00:00:00 2001 From: Navinn Ravindaran Date: Sun, 25 Jul 2021 15:53:38 -0400 Subject: [PATCH 07/10] Add 'No History Found' fallback - Covers git-ignored files edge case --- src/components/HistorySideBar.tsx | 71 +++++++++++++++++-------------- src/style/HistorySideBarStyle.ts | 9 ++++ 2 files changed, 48 insertions(+), 32 deletions(-) diff --git a/src/components/HistorySideBar.tsx b/src/components/HistorySideBar.tsx index e6b17e4b2..b4cea4f60 100644 --- a/src/components/HistorySideBar.tsx +++ b/src/components/HistorySideBar.tsx @@ -7,6 +7,7 @@ import { GitExtension } from '../model'; import { hiddenButtonStyle } from '../style/ActionButtonStyle'; import { historySideBarStyle, + noHistoryFoundStyle, selectedHistoryFileStyle } from '../style/HistorySideBarStyle'; import { ContextCommandIDs, Git } from '../tokens'; @@ -145,40 +146,46 @@ export const HistorySideBar: React.FunctionComponent = ( onDoubleClick={removeSelectedFile} /> )} - {commits.map((commit: Git.ISingleCommitInfo) => { - const commonProps = { - commit, - branches: props.branches, - model: props.model, - commands: props.commands, - trans: props.trans - }; + {commits.length ? ( + commits.map((commit: Git.ISingleCommitInfo) => { + const commonProps = { + commit, + branches: props.branches, + model: props.model, + commands: props.commands, + trans: props.trans + }; - // Only pass down callback when single file history is open - // and its diff is viewable - const onOpenDiff = - props.model.selectedHistoryFile && !commit.is_binary - ? openDiff(commit)( - props.model.selectedHistoryFile.to, - !commit.is_binary - ) - : undefined; + // Only pass down callback when single file history is open + // and its diff is viewable + const onOpenDiff = + props.model.selectedHistoryFile && !commit.is_binary + ? openDiff(commit)( + props.model.selectedHistoryFile.to, + !commit.is_binary + ) + : undefined; - return ( - - {!props.model.selectedHistoryFile && ( - - )} - - ); - })} + return ( + + {!props.model.selectedHistoryFile && ( + + )} + + ); + }) + ) : ( +
      1. + {props.trans.__('No history found for the selected file.')} +
      2. + )}
      ); }; diff --git a/src/style/HistorySideBarStyle.ts b/src/style/HistorySideBarStyle.ts index 17da461dc..54b497fc8 100644 --- a/src/style/HistorySideBarStyle.ts +++ b/src/style/HistorySideBarStyle.ts @@ -14,6 +14,15 @@ export const selectedHistoryFileStyle = style({ backgroundColor: 'var(--jp-toolbar-active-background)' }); +export const noHistoryFoundStyle = style({ + display: 'flex', + justifyContent: 'center', + + padding: '10px 0', + + color: 'var(--jp-ui-font-color2)' +}); + export const historySideBarStyle = style({ display: 'flex', flexDirection: 'column', From c67ebe17a5619754c0da98ddec944207cf690a92 Mon Sep 17 00:00:00 2001 From: Navinn Ravindaran Date: Tue, 27 Jul 2021 09:29:01 -0400 Subject: [PATCH 08/10] Apply suggestions from code review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Improve readability Co-authored-by: Frédéric Collonval --- src/components/HistorySideBar.tsx | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/components/HistorySideBar.tsx b/src/components/HistorySideBar.tsx index b4cea4f60..ab4037dfc 100644 --- a/src/components/HistorySideBar.tsx +++ b/src/components/HistorySideBar.tsx @@ -112,10 +112,13 @@ export const HistorySideBar: React.FunctionComponent = ( const uncommitted = React.useMemo(() => { return { author: props.trans.__('You'), - // INDEX or WORKING special ref - commit: `${+(props.model.selectedHistoryFile?.status === 'staged')}`, + commit: `${ + props.model.selectedHistoryFile?.status === 'staged' + ? Git.Diff.SpecialRef.INDEX + : Git.Diff.SpecialRef.WORKING + }`, pre_commit: 'HEAD', - is_binary: !!props.commits[0]?.is_binary, + is_binary: props.commits[0]?.is_binary ?? false, commit_msg: props.trans.__('Uncommitted Changes'), date: props.trans.__('now') }; From 3418e30f6aff28dccc150dd776001b1184740bc6 Mon Sep 17 00:00:00 2001 From: Navinn Ravindaran Date: Mon, 2 Aug 2021 07:04:03 -0400 Subject: [PATCH 09/10] Fix tests for history (#864) --- jupyterlab_git/tests/test_handlers.py | 4 +- src/components/HistorySideBar.tsx | 2 +- tests/test-components/FileItem.spec.tsx | 2 +- tests/test-components/GitPanel.spec.tsx | 3 + tests/test-components/HistorySideBar.spec.tsx | 57 ++++++++++++++++++- tests/test-components/PastCommitNode.spec.tsx | 38 ++++++------- 6 files changed, 78 insertions(+), 28 deletions(-) diff --git a/jupyterlab_git/tests/test_handlers.py b/jupyterlab_git/tests/test_handlers.py index 44bd9b4cb..ad90f4164 100644 --- a/jupyterlab_git/tests/test_handlers.py +++ b/jupyterlab_git/tests/test_handlers.py @@ -281,7 +281,7 @@ async def test_log_handler(mock_git, jp_fetch, jp_root_dir): ) # Then - mock_git.log.assert_called_with(str(local_path), 20) + mock_git.log.assert_called_with(str(local_path), 20, None) assert response.code == 200 payload = json.loads(response.body) @@ -301,7 +301,7 @@ async def test_log_handler_no_history_count(mock_git, jp_fetch, jp_root_dir): ) # Then - mock_git.log.assert_called_with(str(local_path), 25) + mock_git.log.assert_called_with(str(local_path), 25, None) assert response.code == 200 payload = json.loads(response.body) diff --git a/src/components/HistorySideBar.tsx b/src/components/HistorySideBar.tsx index ab4037dfc..071a78d53 100644 --- a/src/components/HistorySideBar.tsx +++ b/src/components/HistorySideBar.tsx @@ -186,7 +186,7 @@ export const HistorySideBar: React.FunctionComponent = ( }) ) : (
    1. - {props.trans.__('No history found for the selected file.')} + {props.trans.__('No history found.')}
    2. )}
    diff --git a/tests/test-components/FileItem.spec.tsx b/tests/test-components/FileItem.spec.tsx index 4db784ffc..2cf0f6cec 100644 --- a/tests/test-components/FileItem.spec.tsx +++ b/tests/test-components/FileItem.spec.tsx @@ -29,7 +29,7 @@ describe('FileItem', () => { const component = shallow(); it('should display the full path on hover', () => { expect( - component.find('[title="some/file/path/file-name ● Modified"]') + component.find('[title="some/file/path/file-name • Modified"]') ).toHaveLength(1); }); }); diff --git a/tests/test-components/GitPanel.spec.tsx b/tests/test-components/GitPanel.spec.tsx index 8424f9e4d..b3b6b26ff 100644 --- a/tests/test-components/GitPanel.spec.tsx +++ b/tests/test-components/GitPanel.spec.tsx @@ -244,6 +244,9 @@ describe('GitPanel', () => { }, statusChanged: { connect: jest.fn() + }, + selectedHistoryFileChanged: { + connect: jest.fn() } } as any; diff --git a/tests/test-components/HistorySideBar.spec.tsx b/tests/test-components/HistorySideBar.spec.tsx index 2f46ae34f..c7e78e6eb 100644 --- a/tests/test-components/HistorySideBar.spec.tsx +++ b/tests/test-components/HistorySideBar.spec.tsx @@ -7,8 +7,15 @@ import { import 'jest'; import { PastCommitNode } from '../../src/components/PastCommitNode'; +import { GitExtension } from '../../src/model'; +import { nullTranslator } from '@jupyterlab/translation'; +import { FileItem } from '../../src/components/FileItem'; +import { DocumentRegistry } from '@jupyterlab/docregistry'; +import { SinglePastCommitInfo } from '../../src/components/SinglePastCommitInfo'; describe('HistorySideBar', () => { + const trans = nullTranslator.load('jupyterlab-git'); + const props: IHistorySideBarProps = { commits: [ { @@ -20,12 +27,56 @@ describe('HistorySideBar', () => { } ], branches: [], - model: null, + model: { + selectedHistoryFile: null + } as GitExtension, commands: null, - trans: null + trans }; - test('renders commit nodes', () => { + + it('renders the commit nodes', () => { const historySideBar = shallow(); expect(historySideBar.find(PastCommitNode)).toHaveLength(1); + expect(historySideBar.find(SinglePastCommitInfo)).toHaveLength(1); + // Selected history file element + expect(historySideBar.find(FileItem)).toHaveLength(0); + }); + + it('shows a message if no commits are found', () => { + const propsWithoutCommits: IHistorySideBarProps = { ...props, commits: [] }; + const historySideBar = shallow(); + expect(historySideBar.find(PastCommitNode)).toHaveLength(0); + + const noHistoryFound = historySideBar.find('li'); + expect(noHistoryFound).toHaveLength(1); + expect(noHistoryFound.text()).toEqual('No history found.'); + }); + + it('correctly shows the selected history file', () => { + const propsWithSelectedFile: IHistorySideBarProps = { + ...props, + model: { + selectedHistoryFile: { + x: '', + y: '', + to: '/path/to/file', + from: '', + is_binary: null, + status: 'unmodified', + type: {} as DocumentRegistry.IFileType + } + } as GitExtension + }; + + const historySideBar = shallow( + + ); + const selectedHistoryFile = historySideBar.find(FileItem); + expect(selectedHistoryFile).toHaveLength(1); + expect(selectedHistoryFile.prop('file')).toEqual( + propsWithSelectedFile.model.selectedHistoryFile + ); + // Only renders with repository history + expect(historySideBar.find(SinglePastCommitInfo)).toHaveLength(0); }); }); diff --git a/tests/test-components/PastCommitNode.spec.tsx b/tests/test-components/PastCommitNode.spec.tsx index 5c56c8d9d..b883f1f2d 100644 --- a/tests/test-components/PastCommitNode.spec.tsx +++ b/tests/test-components/PastCommitNode.spec.tsx @@ -1,14 +1,16 @@ -import * as React from 'react'; +import { nullTranslator } from '@jupyterlab/translation'; import { shallow } from 'enzyme'; -import { SinglePastCommitInfo } from '../../src/components/SinglePastCommitInfo'; +import 'jest'; +import * as React from 'react'; import { - PastCommitNode, - IPastCommitNodeProps + IPastCommitNodeProps, + PastCommitNode } from '../../src/components/PastCommitNode'; import { Git } from '../../src/tokens'; -import 'jest'; describe('PastCommitNode', () => { + const trans = nullTranslator.load('jupyterlab-git'); + const notMatchingBranches: Git.IBranch[] = [ { is_current_branch: false, @@ -57,7 +59,7 @@ describe('PastCommitNode', () => { }, branches: branches, commands: null, - trans: null + trans }; test('Includes commit info', () => { @@ -76,22 +78,16 @@ describe('PastCommitNode', () => { expect(node.text()).not.toMatch('name2'); }); - test('Doesnt include details at first', () => { - const node = shallow(); - expect(node.find(SinglePastCommitInfo)).toHaveLength(0); - }); - - test('includes details after click', () => { - const node = shallow(); - node.simulate('click'); - expect(node.find(SinglePastCommitInfo)).toHaveLength(1); - }); - - test('hides details after collapse', () => { - const node = shallow(); + test('Toggle show details', () => { + // simulates SinglePastCommitInfo child + const node = shallow( + +
    +
    + ); node.simulate('click'); - expect(node.find(SinglePastCommitInfo)).toHaveLength(1); + expect(node.find('div#singlePastCommitInfo')).toHaveLength(1); node.simulate('click'); - expect(node.find(SinglePastCommitInfo)).toHaveLength(0); + expect(node.find('div#singlePastCommitInfo')).toHaveLength(0); }); }); From 489ac202af15bb262107dcfe54728f78486818d7 Mon Sep 17 00:00:00 2001 From: Navinn Ravindaran Date: Tue, 3 Aug 2021 08:48:52 -0400 Subject: [PATCH 10/10] Add single file log test (#864) --- jupyterlab_git/tests/test_single_file_log.py | 398 +++++++++++++++++++ 1 file changed, 398 insertions(+) create mode 100644 jupyterlab_git/tests/test_single_file_log.py diff --git a/jupyterlab_git/tests/test_single_file_log.py b/jupyterlab_git/tests/test_single_file_log.py new file mode 100644 index 000000000..db2e1d7ba --- /dev/null +++ b/jupyterlab_git/tests/test_single_file_log.py @@ -0,0 +1,398 @@ +from pathlib import Path +from unittest.mock import patch + +import pytest + +from jupyterlab_git.git import Git + +from .testutils import maybe_future + + +@pytest.mark.asyncio +async def test_single_file_log(): + with patch("jupyterlab_git.git.execute") as mock_execute: + # Given + process_output = [ + "8b080cb6cdd526e91199fc003b10e1ba8010f1c4", + "Awesome Developer", + "4 months ago", + "Set _commitAndPush_ to false by default", + "1\t\t1\tREADME.md", + "", + "4fe9688ba74b2cf023af3d76fd737c9fe31548fe", + "Awesome Developer", + "4 months ago", + "Fixes #762 (#914)", + "1\t\t1\tREADME.md", + "", + "4444de0ee37e26d4a7849812f845370b6c7da994", + "Awesome Developer", + "4 months ago", + "Add new setting to README", + "1\t\t0\tREADME.md", + "", + "a216011e407d58bd7ef5c6c1005903a72758016d", + "Awesome Developer", + "5 months ago", + "restore preview gif", + "2\t\t0\tREADME.md", + "", + "2777209f76c70b91ff0ffdd4d878b779758ab355", + "Awesome Developer", + "5 months ago", + "fill out details in troubleshooting", + "1\t\t1\tREADME.md", + "", + "ab35dafe2afb9de3da69590f94028f9582b52862", + "Awesome Developer", + "5 months ago", + "Update README.md:Git panel is not visible (#891)", + "4\t\t1\tREADME.md", + "", + "966671c3e0d4ec62bb4ab03c3a363fbba2ef2666", + "Awesome Developer", + "6 months ago", + "Add some notes for JupyterLab <3 (#865)", + "13\t\t1\tREADME.md", + "", + "c03c1aae4d638cf7ff533ac4065148fa4fce88e0", + "Awesome Developer", + "6 months ago", + "Fix #859", + "2\t\t2\tREADME.md", + "", + "a72d4b761e0701c226b39a822ac42b80a91ad9df", + "Awesome Developer", + "7 months ago", + "Update to Jupyterlab 3.0 (#818)", + "47\t4\t3\tREADME.md", + "", + "a72b9852ed5af0f9368acb6737557960515a6fdb", + "Awesome Developer", + "8 months ago", + "Update README.md", + "3\t\t1\tREADME.md", + "", + "120b04e1a3641eb5ae0a042e3934bd48eb6469df", + "Awesome Developer", + "9 months ago", + "Reduce polling (#812)", + "1\t\t0\tREADME.md", + "", + "7b7f0e53a7a25c0726bbf6f27912918185193b2d", + "Awesome Developer", + "10 months ago", + "Hide overflow on focus (#792)", + "1\t\t1\tREADME.md", + "", + "4efb0a776cd22756b80cae7ec2c1cda5f701ab93", + "Awesome Developer", + "10 months ago", + "Add conda installation instructions", + "12\t\t5\tREADME.md", + "", + "706fb7a8d5cc8f98bcaf61374a8f4ca77f92f056", + "Awesome Developer", + "10 months ago", + "Clarify post_init directions (#781)", + "6\t\t0\tREADME.md", + "", + "70f26818d434e086ca5d5aead8548bdeb0fd6564", + "Awesome Developer", + "11 months ago", + "add black to pre-commit", + "1\t\t0\tREADME.md", + "", + "a046b66415c6afabcbdf6e624e2a367523ee2e80", + "Awesome Developer", + "12 months ago", + "Switch to GitHub action for testing (#738)", + "1\t\t1\tREADME.md", + "", + "6eb994e7fee4a6bc482f641f1265a6fa031112d4", + "Awesome Developer", + "12 months ago", + "Restore server_root endpoint (#733)", + "19\t\t1\tREADME.md", + "", + "5056ca14dd90472b4a3f621d1bc2edd0c30a9a12", + "Awesome Developer", + "1 year ago", + "Check git version and compatibility front/backend version (#658)", + "1\t\t1\tREADME.md", + "", + "ab00273e351d04d1f4be0c89d3943c083bee0b73", + "Awesome Developer", + "1 year ago", + "docs: update README.md [skip ci]", + "1\t\t1\tREADME.md", + "", + "6ffe9fec322ac4f6c126b4d46f145b003c52195d", + "Awesome Developer", + "1 year ago", + "docs: update README.md [skip ci]", + "12\t1\t3\tREADME.md", + "", + "dafc4006d5d3df5b07d4ac0ef045a73ea41577da", + "Awesome Developer", + "1 year ago", + 'Merge PR #630 "Provide UI feedback during Git command execution"', + "4\t\t1\tREADME.md", + "", + "2f9ad78074bd8219200587020236ac77daa761be", + "Awesome Developer", + "1 year, 1 month ago", + "link -> install", + "2\t\t2\tREADME.md", + "", + "bbcc5d8e6b7f5a8abc71b0d473845a55fbbaad42", + "Awesome Developer", + "1 year, 2 months ago", + "add doubleClickDiff setting to readme", + "1\t\t0\tREADME.md", + "", + "8e79eae1277f8a9b8a07123717dbd5cc8ba5f83d", + "Awesome Developer", + "1 year, 2 months ago", + "Switch sponsorship icon (#579)", + "11\t\t7\tREADME.md", + "", + "ec66c24fb7391116ea0d91d5b7a275cf57ff0fe7", + "Awesome Developer", + "1 year, 2 months ago", + "Fix #551 (#649)", + "2\t\t2\tREADME.md", + "", + ] + + mock_execute.return_value = maybe_future((0, "\n".join(process_output), "")) + + expected_response = { + "code": 0, + "commits": [ + { + "commit": "8b080cb6cdd526e91199fc003b10e1ba8010f1c4", + "author": "Awesome Developer", + "date": "4 months ago", + "commit_msg": "Set _commitAndPush_ to false by default", + "pre_commit": "4fe9688ba74b2cf023af3d76fd737c9fe31548fe", + "is_binary": False, + }, + { + "commit": "4fe9688ba74b2cf023af3d76fd737c9fe31548fe", + "author": "Awesome Developer", + "date": "4 months ago", + "commit_msg": "Fixes #762 (#914)", + "pre_commit": "4444de0ee37e26d4a7849812f845370b6c7da994", + "is_binary": False, + }, + { + "commit": "4444de0ee37e26d4a7849812f845370b6c7da994", + "author": "Awesome Developer", + "date": "4 months ago", + "commit_msg": "Add new setting to README", + "pre_commit": "a216011e407d58bd7ef5c6c1005903a72758016d", + "is_binary": False, + }, + { + "commit": "a216011e407d58bd7ef5c6c1005903a72758016d", + "author": "Awesome Developer", + "date": "5 months ago", + "commit_msg": "restore preview gif", + "pre_commit": "2777209f76c70b91ff0ffdd4d878b779758ab355", + "is_binary": False, + }, + { + "commit": "2777209f76c70b91ff0ffdd4d878b779758ab355", + "author": "Awesome Developer", + "date": "5 months ago", + "commit_msg": "fill out details in troubleshooting", + "pre_commit": "ab35dafe2afb9de3da69590f94028f9582b52862", + "is_binary": False, + }, + { + "commit": "ab35dafe2afb9de3da69590f94028f9582b52862", + "author": "Awesome Developer", + "date": "5 months ago", + "commit_msg": "Update README.md:Git panel is not visible (#891)", + "pre_commit": "966671c3e0d4ec62bb4ab03c3a363fbba2ef2666", + "is_binary": False, + }, + { + "commit": "966671c3e0d4ec62bb4ab03c3a363fbba2ef2666", + "author": "Awesome Developer", + "date": "6 months ago", + "commit_msg": "Add some notes for JupyterLab <3 (#865)", + "pre_commit": "c03c1aae4d638cf7ff533ac4065148fa4fce88e0", + "is_binary": False, + }, + { + "commit": "c03c1aae4d638cf7ff533ac4065148fa4fce88e0", + "author": "Awesome Developer", + "date": "6 months ago", + "commit_msg": "Fix #859", + "pre_commit": "a72d4b761e0701c226b39a822ac42b80a91ad9df", + "is_binary": False, + }, + { + "commit": "a72d4b761e0701c226b39a822ac42b80a91ad9df", + "author": "Awesome Developer", + "date": "7 months ago", + "commit_msg": "Update to Jupyterlab 3.0 (#818)", + "pre_commit": "a72b9852ed5af0f9368acb6737557960515a6fdb", + "is_binary": False, + }, + { + "commit": "a72b9852ed5af0f9368acb6737557960515a6fdb", + "author": "Awesome Developer", + "date": "8 months ago", + "commit_msg": "Update README.md", + "pre_commit": "120b04e1a3641eb5ae0a042e3934bd48eb6469df", + "is_binary": False, + }, + { + "commit": "120b04e1a3641eb5ae0a042e3934bd48eb6469df", + "author": "Awesome Developer", + "date": "9 months ago", + "commit_msg": "Reduce polling (#812)", + "pre_commit": "7b7f0e53a7a25c0726bbf6f27912918185193b2d", + "is_binary": False, + }, + { + "commit": "7b7f0e53a7a25c0726bbf6f27912918185193b2d", + "author": "Awesome Developer", + "date": "10 months ago", + "commit_msg": "Hide overflow on focus (#792)", + "pre_commit": "4efb0a776cd22756b80cae7ec2c1cda5f701ab93", + "is_binary": False, + }, + { + "commit": "4efb0a776cd22756b80cae7ec2c1cda5f701ab93", + "author": "Awesome Developer", + "date": "10 months ago", + "commit_msg": "Add conda installation instructions", + "pre_commit": "706fb7a8d5cc8f98bcaf61374a8f4ca77f92f056", + "is_binary": False, + }, + { + "commit": "706fb7a8d5cc8f98bcaf61374a8f4ca77f92f056", + "author": "Awesome Developer", + "date": "10 months ago", + "commit_msg": "Clarify post_init directions (#781)", + "pre_commit": "70f26818d434e086ca5d5aead8548bdeb0fd6564", + "is_binary": False, + }, + { + "commit": "70f26818d434e086ca5d5aead8548bdeb0fd6564", + "author": "Awesome Developer", + "date": "11 months ago", + "commit_msg": "add black to pre-commit", + "pre_commit": "a046b66415c6afabcbdf6e624e2a367523ee2e80", + "is_binary": False, + }, + { + "commit": "a046b66415c6afabcbdf6e624e2a367523ee2e80", + "author": "Awesome Developer", + "date": "12 months ago", + "commit_msg": "Switch to GitHub action for testing (#738)", + "pre_commit": "6eb994e7fee4a6bc482f641f1265a6fa031112d4", + "is_binary": False, + }, + { + "commit": "6eb994e7fee4a6bc482f641f1265a6fa031112d4", + "author": "Awesome Developer", + "date": "12 months ago", + "commit_msg": "Restore server_root endpoint (#733)", + "pre_commit": "5056ca14dd90472b4a3f621d1bc2edd0c30a9a12", + "is_binary": False, + }, + { + "commit": "5056ca14dd90472b4a3f621d1bc2edd0c30a9a12", + "author": "Awesome Developer", + "date": "1 year ago", + "commit_msg": "Check git version and compatibility front/backend version (#658)", + "pre_commit": "ab00273e351d04d1f4be0c89d3943c083bee0b73", + "is_binary": False, + }, + { + "commit": "ab00273e351d04d1f4be0c89d3943c083bee0b73", + "author": "Awesome Developer", + "date": "1 year ago", + "commit_msg": "docs: update README.md [skip ci]", + "pre_commit": "6ffe9fec322ac4f6c126b4d46f145b003c52195d", + "is_binary": False, + }, + { + "commit": "6ffe9fec322ac4f6c126b4d46f145b003c52195d", + "author": "Awesome Developer", + "date": "1 year ago", + "commit_msg": "docs: update README.md [skip ci]", + "pre_commit": "dafc4006d5d3df5b07d4ac0ef045a73ea41577da", + "is_binary": False, + }, + { + "commit": "dafc4006d5d3df5b07d4ac0ef045a73ea41577da", + "author": "Awesome Developer", + "date": "1 year ago", + "commit_msg": 'Merge PR #630 "Provide UI feedback during Git command execution"', + "pre_commit": "2f9ad78074bd8219200587020236ac77daa761be", + "is_binary": False, + }, + { + "commit": "2f9ad78074bd8219200587020236ac77daa761be", + "author": "Awesome Developer", + "date": "1 year, 1 month ago", + "commit_msg": "link -> install", + "pre_commit": "bbcc5d8e6b7f5a8abc71b0d473845a55fbbaad42", + "is_binary": False, + }, + { + "commit": "bbcc5d8e6b7f5a8abc71b0d473845a55fbbaad42", + "author": "Awesome Developer", + "date": "1 year, 2 months ago", + "commit_msg": "add doubleClickDiff setting to readme", + "pre_commit": "8e79eae1277f8a9b8a07123717dbd5cc8ba5f83d", + "is_binary": False, + }, + { + "commit": "8e79eae1277f8a9b8a07123717dbd5cc8ba5f83d", + "author": "Awesome Developer", + "date": "1 year, 2 months ago", + "commit_msg": "Switch sponsorship icon (#579)", + "pre_commit": "ec66c24fb7391116ea0d91d5b7a275cf57ff0fe7", + "is_binary": False, + }, + { + "commit": "ec66c24fb7391116ea0d91d5b7a275cf57ff0fe7", + "author": "Awesome Developer", + "date": "1 year, 2 months ago", + "commit_msg": "Fix #551 (#649)", + "pre_commit": "", + "is_binary": False, + }, + ], + } + + # When + actual_response = await Git().log( + path=str(Path("/bin/test_curr_path")), + history_count=25, + follow_path="README.md", + ) + + # Then + mock_execute.assert_called_once_with( + [ + "git", + "log", + "--pretty=format:%H%n%an%n%ar%n%s", + "-25", + "--numstat", + "--follow", + "--", + "README.md", + ], + cwd=str(Path("/bin") / "test_curr_path"), + ) + + assert expected_response == actual_response