Skip to content

Commit

Permalink
Merge pull request #950 from paugier/mercurial-contentprovider
Browse files Browse the repository at this point in the history
  • Loading branch information
betatim authored Sep 21, 2020
2 parents 09a4cfb + 05002a4 commit 35e6e7e
Show file tree
Hide file tree
Showing 8 changed files with 261 additions and 7 deletions.
3 changes: 3 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,9 @@ jobs:
python setup.py bdist_wheel
pip install dist/*.whl
pip freeze
# hg-evolve pinned to 9.2 because hg-evolve dropped support for
# hg 4.5, installed with apt in Ubuntu 18.04
$(hg debuginstall --template "{pythonexe}") -m pip install hg-evolve==9.2 --user
- name: "Run tests"
run: |
Expand Down
11 changes: 7 additions & 4 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
ARG ALPINE_VERSION=3.9.4
ARG ALPINE_VERSION=3.12.0
FROM alpine:${ALPINE_VERSION}

RUN apk add --no-cache git python3 python3-dev
RUN apk add --no-cache git python3 python3-dev py-pip

# build wheels in first image
ADD . /tmp/src
Expand All @@ -15,8 +15,11 @@ RUN mkdir /tmp/wheelhouse \

FROM alpine:${ALPINE_VERSION}

# install python, git, bash
RUN apk add --no-cache git git-lfs python3 bash docker
# install python, git, bash, mercurial
RUN apk add --no-cache git git-lfs python3 py-pip bash docker mercurial

# install hg-evolve (Mercurial extensions)
RUN pip3 install hg-evolve --user --no-cache-dir

# install repo2docker
COPY --from=0 /tmp/wheelhouse /tmp/wheelhouse
Expand Down
10 changes: 7 additions & 3 deletions docs/source/contributing/tasks.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,15 @@ If you want to run a specific test, you can do so with:
py.test -s tests/<path-to-test>
```

To skip the tests related to Mercurial repositories (to avoid to install
Mercurial and hg-evolve), one can use the environment variable
``REPO2DOCKER_SKIP_HG_TESTS``.

### Troubleshooting Tests

Some of the tests have non-python requirements for your development machine. They are:

- `git-lfs` must be installed ([instructions](https://github.com/git-lfs/git-lfs)). It need not be activated -- there is no need to run the `git lfs install` command. It just needs to be available to the test suite.
- `git-lfs` must be installed ([instructions](https://github.com/git-lfs/git-lfs)). It need not be activated -- there is no need to run the `git lfs install` command. It just needs to be available to the test suite.
- If your test failure messages include "`git-lfs filter-process: git-lfs: command not found`", this step should address the problem.

- Minimum Docker Image size of 128GB is required. If you are not running docker on a linux OS, you may need to expand the runtime image size for your installation. See Docker's instructions for [macOS](https://docs.docker.com/docker-for-mac/space/) or [Windows 10](https://docs.docker.com/docker-for-windows/#resources) for more information.
Expand Down Expand Up @@ -218,7 +222,7 @@ files accordingly.
## Compare generated Dockerfiles between repo2docker versions

For larger refactorings it can be useful to check that the generated Dockerfiles match
between an older version of r2d and the current version. The following shell script
between an older version of r2d and the current version. The following shell script
automates this test.

```bash
Expand All @@ -231,7 +235,7 @@ basename="dockerfilediff"
diff_r2d_dockerfiles_with_version () {
docker run --rm -t -v "$(pwd)":"$(pwd)" --user 1000 jupyterhub/repo2docker:"$1" jupyter-repo2docker --no-build --debug "$(pwd)" &> "$basename"."$1"
jupyter-repo2docker --no-build --debug "$(pwd)" &> "$basename"."$current_version"

# remove first line logging the path
sed -i '/^\[Repo2Docker\]/d' "$basename"."$1"
sed -i '/^\[Repo2Docker\]/d' "$basename"."$current_version"
Expand Down
17 changes: 17 additions & 0 deletions docs/source/install.rst
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,23 @@ The `BinderHub <https://binderhub.readthedocs.io/>`_ helm chart uses version
`helm chart <https://github.com/jupyterhub/binderhub/blob/master/helm-chart/binderhub/values.yaml#L167>`_
for more details.

Optional: Mercurial
-------------------

For `Mercurial <https://www.mercurial-scm.org>`_ repositories, Mercurial and
`hg-evolve <https://www.mercurial-scm.org/doc/evolution/>`_ need to be
installed. For example, on Debian based distributions, one can do::

sudo apt install mercurial
$(hg debuginstall --template "{pythonexe}") -m pip install hg-evolve --user

To install Mercurial on other systems, see `here
<https://www.mercurial-scm.org/download>`_.

Note that for old Mercurial versions, you may need to specify a version for
hg-evolve. For example, ``hg-evolve==9.2`` for hg 4.5 (which is installed with
`apt` on Ubuntu 18.4).

Installing with ``pip``
-----------------------

Expand Down
1 change: 1 addition & 0 deletions repo2docker/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,7 @@ def _default_log_level(self):
contentproviders.Figshare,
contentproviders.Dataverse,
contentproviders.Hydroshare,
contentproviders.Mercurial,
contentproviders.Git,
],
config=True,
Expand Down
1 change: 1 addition & 0 deletions repo2docker/contentproviders/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@
from .figshare import Figshare
from .dataverse import Dataverse
from .hydroshare import Hydroshare
from .mercurial import Mercurial
76 changes: 76 additions & 0 deletions repo2docker/contentproviders/mercurial.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import subprocess

from .base import ContentProvider, ContentProviderException
from ..utils import execute_cmd

args_enabling_topic = ["--config", "extensions.topic="]


class Mercurial(ContentProvider):
"""Provide contents of a remote Mercurial repository."""

def detect(self, source, ref=None, extra_args=None):
if "github.com/" in source or source.endswith(".git"):
return None
try:
subprocess.check_output(
["hg", "identify", source, "--config", "extensions.hggit=!"]
+ args_enabling_topic,
stderr=subprocess.DEVNULL,
)
except subprocess.CalledProcessError:
return None

return {"repo": source, "ref": ref}

def fetch(self, spec, output_dir, yield_output=False):
repo = spec["repo"]
ref = spec.get("ref", None)

# make a clone of the remote repository
try:
cmd = [
"hg",
"clone",
repo,
output_dir,
"--config",
"phases.publish=False",
] + args_enabling_topic
if ref is not None:
# don't update so the clone will include an empty working
# directory, the given ref will be updated out later
cmd.extend(["--noupdate"])
for line in execute_cmd(cmd, capture=yield_output):
yield line

except subprocess.CalledProcessError as error:
msg = f"Failed to clone repository from {repo}"
if ref is not None:
msg += f" (ref {ref})"
msg += "."
raise ContentProviderException(msg) from error

# check out the specific ref given by the user
if ref is not None:
try:
for line in execute_cmd(
["hg", "update", "--clean", ref] + args_enabling_topic,
cwd=output_dir,
capture=yield_output,
):
yield line
except subprocess.CalledProcessError:
self.log.error(
"Failed to update to ref %s", ref, extra=dict(phase="failed")
)
raise ValueError("Failed to update to ref {}".format(ref))

cmd = ["hg", "identify", "-i"] + args_enabling_topic
sha1 = subprocess.Popen(cmd, stdout=subprocess.PIPE, cwd=output_dir)
self._node_id = sha1.stdout.read().decode().strip()

@property
def content_id(self):
"""A unique ID to represent the version of the content."""
return self._node_id
149 changes: 149 additions & 0 deletions tests/unit/contentproviders/test_mercurial.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
from pathlib import Path
import subprocess
from tempfile import TemporaryDirectory
import os
from distutils.util import strtobool

import pytest

from repo2docker.contentproviders import Mercurial
from repo2docker.contentproviders.mercurial import args_enabling_topic

SKIP_HG = strtobool(os.environ.get("REPO2DOCKER_SKIP_HG_TESTS", "False"))

skip_if_no_hg_tests = pytest.mark.skipif(
SKIP_HG,
reason="REPO2DOCKER_SKIP_HG_TESTS",
)


@skip_if_no_hg_tests
def test_if_mercurial_is_available():
"""
To skip the tests related to Mercurial repositories (to avoid to install
Mercurial and hg-evolve), one can use the environment variable
REPO2DOCKER_SKIP_HG_TESTS.
"""
subprocess.check_output(["hg", "version"])


@skip_if_no_hg_tests
def test_if_topic_is_available():
"""Check that the topic extension can be enabled"""
output = subprocess.getoutput("hg version -v --config extensions.topic=")
assert "failed to import extension topic" not in output


def _add_content_to_hg(repo_dir):
"""Add content to file 'test' in hg repository and commit."""
# use append mode so this can be called multiple times
with open(Path(repo_dir) / "test", "a") as f:
f.write("Hello")

def check_call(command):
subprocess.check_call(command + args_enabling_topic, cwd=repo_dir)

check_call(["hg", "add", "test"])
check_call(["hg", "commit", "-m", "Test commit"])
check_call(["hg", "topic", "test-topic"])
check_call(["hg", "commit", "-m", "Test commit in topic test-topic"])
check_call(["hg", "up", "default"])


def _get_node_id(repo_dir):
"""Get repository's current commit node ID (currently SHA1)."""
node_id = subprocess.Popen(
["hg", "identify", "-i"] + args_enabling_topic,
stdout=subprocess.PIPE,
cwd=repo_dir,
)
return node_id.stdout.read().decode().strip()


@pytest.fixture()
def hg_repo():
"""
Make a dummy hg repo in which user can perform hg operations
Should be used as a contextmanager, it will delete directory when done
"""
with TemporaryDirectory() as hgdir:
subprocess.check_call(["hg", "init"], cwd=hgdir)
yield hgdir


@pytest.fixture()
def hg_repo_with_content(hg_repo):
"""Create a hg repository with content"""
_add_content_to_hg(hg_repo)
node_id = _get_node_id(hg_repo)

yield hg_repo, node_id


@skip_if_no_hg_tests
def test_detect_mercurial(hg_repo_with_content, repo_with_content):
mercurial = Mercurial()
assert mercurial.detect("this-is-not-a-directory") is None
assert mercurial.detect("https://github.com/jupyterhub/repo2docker") is None

git_repo = repo_with_content[0]
assert mercurial.detect(git_repo) is None

hg_repo = hg_repo_with_content[0]
assert mercurial.detect(hg_repo) == {"repo": hg_repo, "ref": None}


@skip_if_no_hg_tests
def test_clone(hg_repo_with_content):
"""Test simple hg clone to a target dir"""
upstream, node_id = hg_repo_with_content

with TemporaryDirectory() as clone_dir:
spec = {"repo": upstream}
mercurial = Mercurial()
for _ in mercurial.fetch(spec, clone_dir):
pass
assert (Path(clone_dir) / "test").exists()

assert mercurial.content_id == node_id


@skip_if_no_hg_tests
def test_bad_ref(hg_repo_with_content):
"""
Test trying to update to a ref that doesn't exist
"""
upstream, node_id = hg_repo_with_content
with TemporaryDirectory() as clone_dir:
spec = {"repo": upstream, "ref": "does-not-exist"}
with pytest.raises(ValueError):
for _ in Mercurial().fetch(spec, clone_dir):
pass


@skip_if_no_hg_tests
def test_ref_topic(hg_repo_with_content):
"""
Test trying to update to a topic
To skip this test (to avoid to install Mercurial and hg-evolve), one can
use the environment variable REPO2DOCKER_SKIP_HG_TESTS.
"""
upstream, node_id = hg_repo_with_content
node_id = subprocess.Popen(
["hg", "identify", "-i", "-r", "topic(test-topic)"] + args_enabling_topic,
stdout=subprocess.PIPE,
cwd=upstream,
)
node_id = node_id.stdout.read().decode().strip()

with TemporaryDirectory() as clone_dir:
spec = {"repo": upstream, "ref": "test-topic"}
mercurial = Mercurial()
for _ in mercurial.fetch(spec, clone_dir):
pass
assert (Path(clone_dir) / "test").exists()

assert mercurial.content_id == node_id

0 comments on commit 35e6e7e

Please sign in to comment.