Skip to content

Commit

Permalink
Add submodules display (#1374)
Browse files Browse the repository at this point in the history
* Get submodule names tokens/model/handlers

* Fix submodule return

* Remove null

* Add submodules to UI

* Temp spot to get submodule list

* Update submodule command

* Make submodule display conditional

* Move where listSubModules() gets called

* Add submodules to model in test

* Add doc comments

* Have GitPanel manage submodule state

* Add/update tests

* Add header and style

* Fix submodule fetching when update hasn't been run

* Change casing for submodules handler

* Change casing
  • Loading branch information
gjmooney authored Nov 6, 2024
1 parent dc7271e commit 6b62f1f
Show file tree
Hide file tree
Showing 12 changed files with 391 additions and 20 deletions.
20 changes: 20 additions & 0 deletions jupyterlab_git/git.py
Original file line number Diff line number Diff line change
Expand Up @@ -2183,6 +2183,26 @@ async def apply_stash(self, path: str, stash_index: Optional[int] = None) -> dic

return {"code": code, "message": output.strip()}

async def submodule(self, path):
"""
Execute git submodule status --recursive
"""

cmd = ["git", "submodule", "status", "--recursive"]

code, output, error = await self.__execute(cmd, cwd=path)

results = []

for line in output.splitlines():
name = line.strip().split(" ")[1]
submodule = {
"name": name,
}
results.append(submodule)

return {"code": code, "submodules": results, "error": error}

@property
def excluded_paths(self) -> List[str]:
"""Wildcard-style path patterns that do not support git commands.
Expand Down
25 changes: 22 additions & 3 deletions jupyterlab_git/handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
Module with all the individual handlers, which execute git commands and return the results to the frontend.
"""

import fnmatch
import functools
import json
import os
Expand All @@ -11,9 +12,8 @@
import tornado
from jupyter_server.base.handlers import APIHandler, path_regex
from jupyter_server.services.contents.manager import ContentsManager
from jupyter_server.utils import url2path, url_path_join, ensure_async
from jupyter_server.utils import ensure_async, url2path, url_path_join
from packaging.version import parse
import fnmatch

try:
import hybridcontents
Expand Down Expand Up @@ -915,7 +915,7 @@ async def post(self, path: str = ""):

class GitNewTagHandler(GitHandler):
"""
Hadler for 'git tag <tag_name> <commit_id>. Create new tag pointing to a specific commit.
Handler for 'git tag <tag_name> <commit_id>. Create new tag pointing to a specific commit.
"""

@tornado.web.authenticated
Expand Down Expand Up @@ -1069,6 +1069,24 @@ async def post(self, path: str = ""):
self.finish(json.dumps(response))


class GitSubmodulesHandler(GitHandler):
"""
Handler for 'git submodule status --recursive.
Get a list of submodules in the repo.
"""

@tornado.web.authenticated
async def get(self, path: str = ""):
"""
GET request handler, fetches all submodules in current repository.
"""
result = await self.git.submodule(self.url2localpath(path))

if result["code"] != 0:
self.set_status(500)
self.finish(json.dumps(result))


def setup_handlers(web_app):
"""
Setups all of the git command handlers.
Expand Down Expand Up @@ -1113,6 +1131,7 @@ def setup_handlers(web_app):
("/stash", GitStashHandler),
("/stash_pop", GitStashPopHandler),
("/stash_apply", GitStashApplyHandler),
("/submodules", GitSubmodulesHandler),
]

handlers = [
Expand Down
5 changes: 3 additions & 2 deletions src/__tests__/test-components/GitPanel.spec.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import * as apputils from '@jupyterlab/apputils';
import { nullTranslator } from '@jupyterlab/translation';
import { CommandRegistry } from '@lumino/commands';
import { JSONObject } from '@lumino/coreutils';
import '@testing-library/jest-dom';
import { RenderResult, render, screen, waitFor } from '@testing-library/react';
Expand All @@ -10,12 +11,11 @@ import { GitPanel, IGitPanelProps } from '../../components/GitPanel';
import * as git from '../../git';
import { GitExtension as GitModel } from '../../model';
import {
defaultMockedResponses,
DEFAULT_REPOSITORY_PATH,
IMockedResponse,
defaultMockedResponses,
mockedRequestAPI
} from '../utils';
import { CommandRegistry } from '@lumino/commands';

jest.mock('../../git');
jest.mock('@jupyterlab/apputils');
Expand Down Expand Up @@ -372,6 +372,7 @@ describe('GitPanel', () => {
beforeEach(() => {
props.model = {
branches: [],
submodules: [],
status: {},
stashChanged: {
connect: jest.fn()
Expand Down
70 changes: 70 additions & 0 deletions src/__tests__/test-components/SubModuleMenu.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { nullTranslator } from '@jupyterlab/translation';
import '@testing-library/jest-dom';
import { render, screen } from '@testing-library/react';
import 'jest';
import * as React from 'react';
import {
ISubmoduleMenuProps,
SubmoduleMenu
} from '../../components/SubmoduleMenu';
import { GitExtension } from '../../model';
import { IGitExtension } from '../../tokens';
import { DEFAULT_REPOSITORY_PATH } from '../utils';

jest.mock('../../git');
jest.mock('@jupyterlab/apputils');

const SUBMODULES = [
{
name: 'cli/bench'
},
{
name: 'test/util'
}
];

async function createModel() {
const model = new GitExtension();
model.pathRepository = DEFAULT_REPOSITORY_PATH;

await model.ready;
return model;
}

describe('Submodule Menu', () => {
let model: GitExtension;
const trans = nullTranslator.load('jupyterlab_git');

beforeEach(async () => {
jest.restoreAllMocks();

model = await createModel();
});

function createProps(
props?: Partial<ISubmoduleMenuProps>
): ISubmoduleMenuProps {
return {
model: model as IGitExtension,
trans: trans,
submodules: SUBMODULES,
...props
};
}

describe('render', () => {
it('should display a list of submodules', () => {
render(<SubmoduleMenu {...createProps()} />);

const submodules = SUBMODULES;
expect(screen.getAllByRole('listitem').length).toEqual(submodules.length);

// Should contain the submodule names...
for (let i = 0; i < submodules.length; i++) {
expect(
screen.getByText(submodules[i].name, { exact: true })
).toBeDefined();
}
});
});
});
1 change: 1 addition & 0 deletions src/__tests__/test-components/Toolbar.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ describe('Toolbar', () => {
execute: jest.fn()
} as any,
trans: trans,
submodules: model.submodules,
...props
};
}
Expand Down
22 changes: 20 additions & 2 deletions src/components/GitPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,11 @@ export interface IGitPanelState {
*
*/
stash: Git.IStash[];

/**
* List of submodules.
*/
submodules: Git.ISubmodule[];
}

/**
Expand All @@ -175,7 +180,8 @@ export class GitPanel extends React.Component<IGitPanelProps, IGitPanelState> {
pathRepository,
hasDirtyFiles: hasDirtyStagedFiles,
stash,
tagsList
tagsList,
submodules: submodules
} = props.model;

this.state = {
Expand All @@ -195,7 +201,8 @@ export class GitPanel extends React.Component<IGitPanelProps, IGitPanelState> {
referenceCommit: null,
challengerCommit: null,
stash: stash,
tagsList: tagsList
tagsList: tagsList,
submodules: submodules
};
}

Expand Down Expand Up @@ -246,6 +253,9 @@ export class GitPanel extends React.Component<IGitPanelProps, IGitPanelState> {
model.remoteChanged.connect((_, args) => {
this.warningDialog(args!);
}, this);
model.repositoryChanged.connect(async () => {
await this.refreshSubmodules();
}, this);

settings.changed.connect(this.refreshView, this);

Expand Down Expand Up @@ -300,6 +310,13 @@ export class GitPanel extends React.Component<IGitPanelProps, IGitPanelState> {
}
};

refreshSubmodules = async (): Promise<void> => {
await this.props.model.listSubmodules();
this.setState({
submodules: this.props.model.submodules
});
};

/**
* Refresh widget, update all content
*/
Expand Down Expand Up @@ -410,6 +427,7 @@ export class GitPanel extends React.Component<IGitPanelProps, IGitPanelState> {
nCommitsBehind={this.state.nCommitsBehind}
repository={this.state.repository || ''}
trans={this.props.trans}
submodules={this.state.submodules}
/>
);
}
Expand Down
125 changes: 125 additions & 0 deletions src/components/SubmoduleMenu.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import { TranslationBundle } from '@jupyterlab/translation';
import ListItem from '@mui/material/ListItem';
import * as React from 'react';
import { FixedSizeList, ListChildComponentProps } from 'react-window';
import {
listItemClass,
listItemIconClass,
nameClass,
wrapperClass
} from '../style/BranchMenu';
import { submoduleHeaderStyle } from '../style/SubmoduleMenuStyle';
import { desktopIcon } from '../style/icons';
import { Git, IGitExtension } from '../tokens';

const ITEM_HEIGHT = 24.8; // HTML element height for a single item
const MIN_HEIGHT = 150; // Minimal HTML element height for the list
const MAX_HEIGHT = 400; // Maximal HTML element height for the list

/**
* Interface describing component properties.
*/
export interface ISubmoduleMenuProps {
/**
* Git extension data model.
*/
model: IGitExtension;

/**
* The list of submodules in the repo
*/
submodules: Git.ISubmodule[];

/**
* The application language translator.
*/
trans: TranslationBundle;
}

/**
* Interface describing component state.
*/
export interface ISubmoduleMenuState {}

/**
* React component for rendering a submodule menu.
*/
export class SubmoduleMenu extends React.Component<
ISubmoduleMenuProps,
ISubmoduleMenuState
> {
/**
* Returns a React component for rendering a submodule menu.
*
* @param props - component properties
* @returns React component
*/
constructor(props: ISubmoduleMenuProps) {
super(props);
}

/**
* Renders the component.
*
* @returns React element
*/
render(): React.ReactElement {
return <div className={wrapperClass}>{this._renderSubmoduleList()}</div>;
}

/**
* Renders list of submodules.
*
* @returns React element
*/
private _renderSubmoduleList(): React.ReactElement {
const submodules = this.props.submodules;

return (
<>
<div className={submoduleHeaderStyle}>Submodules</div>
<FixedSizeList
height={Math.min(
Math.max(MIN_HEIGHT, submodules.length * ITEM_HEIGHT),
MAX_HEIGHT
)}
itemCount={submodules.length}
itemData={submodules}
itemKey={(index, data) => data[index].name}
itemSize={ITEM_HEIGHT}
style={{
overflowX: 'hidden',
paddingTop: 0,
paddingBottom: 0
}}
width={'auto'}
>
{this._renderItem}
</FixedSizeList>
</>
);
}

/**
* Renders a menu item.
*
* @param props Row properties
* @returns React element
*/
private _renderItem = (props: ListChildComponentProps): JSX.Element => {
const { data, index, style } = props;
const submodule = data[index] as Git.ISubmodule;

return (
<ListItem
title={this.props.trans.__('Submodule: %1', submodule.name)}
className={listItemClass}
role="listitem"
style={style}
>
<desktopIcon.react className={listItemIconClass} tag="span" />
<span className={nameClass}>{submodule.name}</span>
</ListItem>
);
};
}
Loading

0 comments on commit 6b62f1f

Please sign in to comment.