Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Custom actions #700

Merged
merged 4 commits into from
Aug 12, 2020
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
22 changes: 21 additions & 1 deletion jupyterlab_git/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,28 @@
"""
# need this in order to show version in `jupyter serverextension list`
from ._version import __version__
from traitlets import List, Dict, Unicode
from traitlets.config import Configurable

from jupyterlab_git.handlers import setup_handlers
from jupyterlab_git.git import Git

class JupyterLabGit(Configurable):
"""
Config options for jupyterlab_git

Modeled after: https://github.com/jupyter/jupyter_server/blob/9dd2a9a114c045cfd8fd8748400c6a697041f7fa/jupyter_server/serverapp.py#L1040
"""

actions = Dict(
help='Actions to be taken after a git command. Each action takes a list of commands to execute (strings). Supported actions: post_init',
config=True,
trait=List(
trait=Unicode(),
help='List of commands to run. E.g. ["touch baz.py"]'
)
# TODO Validate
)

def _jupyter_server_extension_paths():
"""Declare the Jupyter server extension paths.
Expand All @@ -16,6 +34,8 @@ def _jupyter_server_extension_paths():
def load_jupyter_server_extension(nbapp):
"""Load the Jupyter server extension.
"""
git = Git(nbapp.web_app.settings['contents_manager'])

config = JupyterLabGit(config=nbapp.config)
git = Git(nbapp.web_app.settings['contents_manager'], config)
nbapp.web_app.settings["git"] = git
setup_handlers(nbapp.web_app)
59 changes: 49 additions & 10 deletions jupyterlab_git/git.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"""
import os
import re
import shlex
import subprocess
from urllib.parse import unquote

Expand Down Expand Up @@ -142,9 +143,10 @@ class Git:
A single parent class containing all of the individual git methods in it.
"""

def __init__(self, contents_manager):
def __init__(self, contents_manager, config=None):
self.contents_manager = contents_manager
self.root_dir = os.path.expanduser(contents_manager.root_dir)
self._config = config

async def config(self, top_repo_path, **kwargs):
"""Get or set Git options.
Expand Down Expand Up @@ -301,7 +303,7 @@ async def status(self, current_path):
for line in filter(lambda l: len(l) > 0, strip_and_split(text_output)):
diff, name = line.rsplit("\t", maxsplit=1)
are_binary[name] = diff.startswith("-\t-")

result = []
line_iterable = (line for line in strip_and_split(my_output) if line)
for line in line_iterable:
Expand Down Expand Up @@ -872,13 +874,50 @@ async def init(self, current_path):
Execute git init command & return the result.
"""
cmd = ["git", "init"]
cwd = os.path.join(self.root_dir, current_path)
code, _, error = await execute(
cmd, cwd=os.path.join(self.root_dir, current_path)
cmd, cwd=cwd
)

actions = None
if code == 0:
code, actions = await self._maybe_run_actions('post_init', cwd)

if code != 0:
return {"code": code, "command": " ".join(cmd), "message": error}
return {"code": code}
return {"code": code, "command": " ".join(cmd), "message": error, "actions": actions}
return {"code": code, "actions": actions}

async def _maybe_run_actions(self, name, cwd):
code = 0
actions = None
if self._config and name in self._config.actions:
actions = []
actions_list = self._config.actions[name]
for action in actions_list:
try:
# We trust the actions as they were passed via a config and not the UI
code, stdout, stderr = await execute(
shlex.split(action), cwd=cwd
)
actions.append({
'cmd': action,
'code': code,
'stdout': stdout,
'stderr': stderr
})
# After any failure, stop
except Exception as e:
code = 1
actions.append({
'cmd': action,
'code': 1,
'stdout': None,
'stderr': 'Exception: {}'.format(e)
})
if code != 0:
break

return code, actions

def _is_remote_branch(self, branch_reference):
"""Check if given branch is remote branch by comparing with 'remotes/',
Expand Down Expand Up @@ -1065,7 +1104,7 @@ async def _is_binary(self, filename, ref, top_repo_path):

Returns:
bool: Is file binary?

Raises:
HTTPError: if git command failed
"""
Expand Down Expand Up @@ -1112,7 +1151,7 @@ def remote_add(self, top_repo_path, url, name=DEFAULT_REMOTE_NAME):

async def version(self):
"""Return the Git command version.

If an error occurs, return None.
"""
command = ["git", "--version"]
Expand All @@ -1121,12 +1160,12 @@ async def version(self):
version = GIT_VERSION_REGEX.match(output)
if version is not None:
return version.group('version')

return None

async def tags(self, current_path):
"""List all tags of the git repository.

current_path: str
Git path repository
"""
Expand All @@ -1139,7 +1178,7 @@ async def tags(self, current_path):

async def tag_checkout(self, current_path, tag):
"""Checkout the git repository at a given tag.

current_path: str
Git path repository
tag : str
Expand Down
122 changes: 122 additions & 0 deletions jupyterlab_git/tests/test_init.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import os
from subprocess import CalledProcessError
from unittest.mock import Mock, call, patch

import pytest
import tornado

from jupyterlab_git import JupyterLabGit
from jupyterlab_git.git import Git

from .testutils import FakeContentManager, maybe_future


@pytest.mark.asyncio
async def test_init():
with patch("jupyterlab_git.git.execute") as mock_execute:
# Given
mock_execute.return_value = maybe_future((0, "", ""))

# When
actual_response = await Git(FakeContentManager("/bin")).init("test_curr_path")

mock_execute.assert_called_once_with(
["git", "init"], cwd=os.path.join("/bin", "test_curr_path")
)

assert {"code": 0, "actions": None} == actual_response


@pytest.mark.asyncio
async def test_init_and_post_init():
with patch("jupyterlab_git.git.execute") as mock_execute:
# Given
mock_execute.side_effect = [
maybe_future((0, "", "")),
maybe_future((0, "hello", "")),
]

# When
actual_response = await Git(
FakeContentManager("/bin"),
JupyterLabGit(actions={"post_init": ['echo "hello"']}),
).init("test_curr_path")

mock_execute.assert_called_with(
["echo", "hello"], cwd=os.path.join("/bin", "test_curr_path")
)

assert {
"code": 0,
"actions": [
{"cmd": 'echo "hello"', "code": 0, "stderr": "", "stdout": "hello"}
],
} == actual_response


@pytest.mark.asyncio
async def test_init_and_post_init_fail():
with patch("jupyterlab_git.git.execute") as mock_execute:
# Given
mock_execute.side_effect = [
maybe_future((0, "", "")),
maybe_future((1, "", "not_there: command not found")),
mlucool marked this conversation as resolved.
Show resolved Hide resolved
]

# When
actual_response = await Git(
FakeContentManager("/bin"),
JupyterLabGit(actions={"post_init": ["not_there arg"]}),
).init("test_curr_path")

mock_execute.assert_called_with(
["not_there", "arg"], cwd=os.path.join("/bin", "test_curr_path")
)

assert {
"code": 1,
"message": "",
"command": "git init",
"actions": [
{
"stderr": "not_there: command not found",
"stdout": "",
"code": 1,
"cmd": "not_there arg",
}
],
} == actual_response


@pytest.mark.asyncio
async def test_init_and_post_init_fail_to_run():
with patch("jupyterlab_git.git.execute") as mock_execute:
# Given
mock_execute.side_effect = [
maybe_future((0, "", "")),
Exception("Not a command!"),
]

# When
actual_response = await Git(
FakeContentManager("/bin"),
JupyterLabGit(actions={"post_init": ["not_there arg"]}),
).init("test_curr_path")

mock_execute.assert_called_with(
["not_there", "arg"], cwd=os.path.join("/bin", "test_curr_path")
)

assert {
"code": 1,
"message": "",
"command": "git init",
"actions": [
{
"stderr": "Exception: Not a command!",
"stdout": None,
"code": 1,
"cmd": "not_there arg",
}
],
} == actual_response