Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading