diff --git a/jupyterlab_git/git.py b/jupyterlab_git/git.py index 24a8c44ea..e91f9d00c 100644 --- a/jupyterlab_git/git.py +++ b/jupyterlab_git/git.py @@ -272,7 +272,7 @@ async def changed_files(self, path, base=None, remote=None, single_commit=None): return response - async def clone(self, path, repo_url, auth=None, versioning=True): + async def clone(self, path, repo_url, auth=None, versioning=True, submodules=False): """ Execute `git clone`. When no auth is provided, disables prompts for the password to avoid the terminal hanging. @@ -281,6 +281,7 @@ async def clone(self, path, repo_url, auth=None, versioning=True): :param repo_url: the URL of the repository to be cloned. :param auth: OPTIONAL dictionary with 'username' and 'password' fields :param versioning: OPTIONAL whether to clone or download a snapshot of the remote repository; default clone + :param submodules: OPTIONAL whether to clone submodules content; default False :return: response with status code and error message. """ env = os.environ.copy() @@ -288,6 +289,8 @@ async def clone(self, path, repo_url, auth=None, versioning=True): if not versioning: cmd.append("--depth=1") current_content = set(os.listdir(path)) + if submodules: + cmd.append("--recurse-submodules") cmd.append(unquote(repo_url)) if auth: diff --git a/jupyterlab_git/handlers.py b/jupyterlab_git/handlers.py index b1e7be2cc..6cd647d42 100644 --- a/jupyterlab_git/handlers.py +++ b/jupyterlab_git/handlers.py @@ -89,6 +89,7 @@ async def post(self, path: str = ""): data["clone_url"], data.get("auth", None), data["versioning"], + data["submodules"], ) if response["code"] != 0: diff --git a/jupyterlab_git/tests/test_clone.py b/jupyterlab_git/tests/test_clone.py index 6aea501d7..562d5a3a5 100644 --- a/jupyterlab_git/tests/test_clone.py +++ b/jupyterlab_git/tests/test_clone.py @@ -64,6 +64,30 @@ def create_fake_git_repo(*args, **kwargs): assert not git_folder.exists() +@pytest.mark.asyncio +async def test_git_submodules_success(tmp_path): + with patch("os.environ", {"TEST": "test"}): + with patch("jupyterlab_git.git.execute") as mock_execute: + # Given + output = "output" + mock_execute.return_value = maybe_future((0, output, "error")) + + # When + actual_response = await Git().clone( + path=str(Path("/bin/test_curr_path")), + repo_url="ghjkhjkl", + submodules=True, + ) + + # Then + mock_execute.assert_called_once_with( + ["git", "clone", "--recurse-submodules", "ghjkhjkl"], + cwd=str(Path("/bin") / "test_curr_path"), + env={"TEST": "test", "GIT_TERMINAL_PROMPT": "0"}, + ) + assert {"code": 0, "message": output} == actual_response + + @pytest.mark.asyncio async def test_git_clone_failure_from_git(): """ diff --git a/src/widgets/GitCloneForm.ts b/src/widgets/GitCloneForm.ts index 040184aee..c42c47990 100644 --- a/src/widgets/GitCloneForm.ts +++ b/src/widgets/GitCloneForm.ts @@ -16,7 +16,7 @@ export class GitCloneForm extends Widget { /** * Returns the input value. */ - getValue(): { url: string; versioning: boolean } { + getValue(): { url: string; versioning: boolean; submodules: boolean } { return { url: encodeURIComponent( ( @@ -25,7 +25,12 @@ export class GitCloneForm extends Widget { ), versioning: Boolean( encodeURIComponent( - (this.node.querySelector('#checkbox') as HTMLInputElement).checked + (this.node.querySelector('#download') as HTMLInputElement).checked + ) + ), + submodules: Boolean( + encodeURIComponent( + (this.node.querySelector('#submodules') as HTMLInputElement).checked ) ) }; @@ -38,33 +43,48 @@ export class GitCloneForm extends Widget { const inputLink = document.createElement('input'); const linkText = document.createElement('span'); const checkboxWrapper = document.createElement('div'); - const checkboxLabel = document.createElement('label'); - const checkbox = document.createElement('input'); + const subModulesLabel = document.createElement('label'); + const subModules = document.createElement('input'); + const downloadLabel = document.createElement('label'); + const download = document.createElement('input'); node.className = 'jp-CredentialsBox'; inputWrapper.className = 'jp-RedirectForm'; checkboxWrapper.className = 'jp-CredentialsBox-wrapper'; - checkboxLabel.className = 'jp-CredentialsBox-label-checkbox'; - checkbox.id = 'checkbox'; + subModulesLabel.className = 'jp-CredentialsBox-label-checkbox'; + downloadLabel.className = 'jp-CredentialsBox-label-checkbox'; + subModules.id = 'submodules'; + download.id = 'download'; inputLink.id = 'input-link'; linkText.textContent = trans.__( 'Enter the URI of the remote Git repository' ); inputLink.placeholder = 'https://host.com/org/repo.git'; - checkboxLabel.textContent = trans.__('Download the repository'); - checkboxLabel.title = trans.__( + + subModulesLabel.textContent = trans.__('Include submodules'); + subModulesLabel.title = trans.__( + 'If checked, the remote submodules in the repository will be cloned recursively' + ); + subModules.setAttribute('type', 'checkbox'); + subModules.setAttribute('checked', 'checked'); + + downloadLabel.textContent = trans.__('Download the repository'); + downloadLabel.title = trans.__( 'If checked, the remote repository default branch will be downloaded instead of cloned' ); - checkbox.setAttribute('type', 'checkbox'); + download.setAttribute('type', 'checkbox'); inputLinkLabel.appendChild(linkText); inputLinkLabel.appendChild(inputLink); inputWrapper.append(inputLinkLabel); - checkboxLabel.prepend(checkbox); - checkboxWrapper.appendChild(checkboxLabel); + subModulesLabel.prepend(subModules); + checkboxWrapper.appendChild(subModulesLabel); + + downloadLabel.prepend(download); + checkboxWrapper.appendChild(downloadLabel); node.appendChild(inputWrapper); node.appendChild(checkboxWrapper);