diff --git a/.github/dependabot.yaml b/.github/dependabot.yaml index 731b0aa6..c3ea9a00 100644 --- a/.github/dependabot.yaml +++ b/.github/dependabot.yaml @@ -8,8 +8,9 @@ version: 2 updates: # Maintain dependencies in our GitHub Workflows - package-ecosystem: github-actions - directory: "/" + directory: / + labels: [ci] schedule: interval: monthly time: "05:00" - timezone: "Etc/UTC" + timezone: Etc/UTC diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 287696db..3ca53fcc 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -26,8 +26,8 @@ jobs: runs-on: ubuntu-22.04 steps: - - uses: actions/checkout@v3 - - uses: actions/setup-python@v4 + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 with: python-version: "3.11" @@ -39,31 +39,3 @@ jobs: run: | cd docs make linkcheck - - build-and-publish: - runs-on: ubuntu-22.04 - - permissions: - # required to push to the gh-pages branch - contents: write - - steps: - - uses: actions/checkout@v3 - - uses: actions/setup-python@v4 - with: - python-version: "3.11" - - - name: Install dependencies - run: | - pip install -r docs/doc-requirements.txt - - - name: make html (Builds documentation) - run: | - cd docs - make html - - - name: Publish to GitHub Pages - if: github.ref == 'refs/heads/main' - run: | - pip install ghp-import - ghp-import --no-jekyll --push --message "Update documentation [skip ci]" docs/_build/html diff --git a/.github/workflows/publish.yml b/.github/workflows/release.yaml similarity index 51% rename from .github/workflows/publish.yml rename to .github/workflows/release.yaml index 04485db9..6eca1877 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/release.yaml @@ -1,18 +1,20 @@ # This is a GitHub workflow defining a set of jobs with a set of steps. -# ref: https://docs.github.com/en/actions/learn-github-actions/workflow-syntax-for-github-actions +# ref: https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions # -# Build releases and publish to PyPI if a tag is pushed name: Release +# Always tests wheel building, but only publish to PyPI on pushed tags. on: pull_request: paths-ignore: - "docs/**" - - "**/docs.yml" + - ".github/workflows/*.yaml" + - "!.github/workflows/release.yaml" push: paths-ignore: - "docs/**" - - "**/docs.yml" + - ".github/workflows/*.yaml" + - "!.github/workflows/release.yaml" branches-ignore: - "dependabot/**" - "pre-commit-ci-update-config" @@ -22,12 +24,20 @@ on: jobs: build-release: runs-on: ubuntu-22.04 + permissions: + # id-token=write is required for pypa/gh-action-pypi-publish, and the PyPI + # project needs to be configured to trust this workflow. + # + # ref: https://github.com/jupyterhub/team-compass/issues/648 + # + id-token: write + steps: - - uses: actions/checkout@v3 - - uses: actions/setup-python@v4 + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 with: python-version: "3.11" - - uses: actions/setup-node@v3 + - uses: actions/setup-node@v4 with: node-version: "18" @@ -43,8 +53,5 @@ jobs: ls -l dist - name: publish to pypi - uses: pypa/gh-action-pypi-publish@v1.8.1 + uses: pypa/gh-action-pypi-publish@release/v1 if: startsWith(github.ref, 'refs/tags/') - with: - user: __token__ - password: ${{ secrets.pypi_password }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d8112c7b..6e09290c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -26,17 +26,47 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"] - node-version: ["16"] + include: + - python-version: "3.7" + pip-install: "jupyter_server==1.* notebook==5.*" + - python-version: "3.8" + pip-install: "jupyter_server==1.* notebook==6.*" + # 2.17 is in ubuntu 18.04 + git-version: "2.17" + - python-version: "3.9" + # 2.25 is in ubuntu 20.04 + git-version: "2.25" + - python-version: "3.10" + # 2.34 is in ubuntu 22.04 + git-version: "2.34" + - python-version: "3.11" + - python-version: "3.12" + # 2.43 is in ubuntu 24.04 + git-version: "2.43" steps: - - uses: actions/checkout@v3 - - uses: actions/setup-python@v4 + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 with: python-version: "${{ matrix.python-version }}" - - uses: actions/setup-node@v3 + + - uses: actions/setup-node@v4 with: - node-version: "${{ matrix.node-version }}" + node-version: "lts/*" + + - name: install git ${{ matrix.git-version }} + if: ${{ matrix.git-version }} + run: | + export MAMBA_ROOT_PREFIX=$/tmp/conda + mkdir -p $MAMBA_ROOT_PREFIX/bin + curl -Ls https://micro.mamba.pm/api/micromamba/linux-64/1.5.8 | tar -xvj -C $MAMBA_ROOT_PREFIX/bin/ --strip-components=1 bin/micromamba + $MAMBA_ROOT_PREFIX/bin/micromamba install -c conda-forge -p $MAMBA_ROOT_PREFIX "git=${{ matrix.git-version }}" + echo "PATH=$MAMBA_ROOT_PREFIX/bin:$PATH" >> $GITHUB_ENV + + - name: git version + run: | + which git + git --version - name: Run webpack to build static assets run: | @@ -45,10 +75,12 @@ jobs: - name: Install dependencies run: | - pip install -r dev-requirements.txt - pip install . + pip install -r dev-requirements.txt ${{ matrix.pip-install }} . + + - name: List dependencies + run: | pip freeze - name: Run tests run: | - pytest --verbose --maxfail=2 --color=yes --cov nbgitpuller + pytest --maxfail=2 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index c14893ff..561aca8e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -45,7 +45,7 @@ repos: # Autoformat: markdown, yaml, javascript (see the file .prettierignore) - repo: https://github.com/pre-commit/mirrors-prettier - rev: v3.0.0-alpha.6 + rev: v4.0.0-alpha.8 hooks: - id: prettier # FIXME: Autoformatting of our .js files is initially not enabled as it @@ -56,7 +56,7 @@ repos: # Autoformat and linting, misc. details - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.4.0 + rev: v4.6.0 hooks: - id: end-of-file-fixer - id: requirements-txt-fixer @@ -65,7 +65,7 @@ repos: # Linting: Python code (see the file .flake8) - repo: https://github.com/PyCQA/flake8 - rev: "6.0.0" + rev: "7.1.1" hooks: - id: flake8 diff --git a/.readthedocs.yaml b/.readthedocs.yaml new file mode 100644 index 00000000..b165d7e5 --- /dev/null +++ b/.readthedocs.yaml @@ -0,0 +1,17 @@ +# Configuration on how ReadTheDocs (RTD) builds our documentation +# ref: https://readthedocs.org/projects/nbgitpuller/ +# ref: https://docs.readthedocs.io/en/stable/config-file/v2.html +# +version: 2 + +sphinx: + configuration: docs/conf.py + +build: + os: ubuntu-20.04 + tools: + python: "3.10" + +python: + install: + - requirements: docs/doc-requirements.txt diff --git a/CHANGELOG.md b/CHANGELOG.md index 7a804ac0..d85bb293 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,77 @@ -## 1.0 +## 1.2 + +### 1.2.1 - 2024-03-29 + +This release provides compatibility with JupyterHub >=4.1. + +([full changelog](https://github.com/jupyterhub/nbgitpuller/compare/1.2.0...1.2.1)) + +#### Bugs fixed + +- 403 on failed auth for EventStream [#347](https://github.com/jupyterhub/nbgitpuller/pull/347) ([@minrk](https://github.com/minrk), [@consideRatio](https://github.com/consideRatio)) +- include xsrf token in event stream request [#346](https://github.com/jupyterhub/nbgitpuller/pull/346) ([@minrk](https://github.com/minrk), [@consideRatio](https://github.com/consideRatio), [@yuvipanda](https://github.com/yuvipanda)) + +#### Maintenance and upkeep improvements + +- Add test for Python 3.12, jupyter_server 1, notebook 5 and 7, and git 2.43 (ubuntu 24.04) [#345](https://github.com/jupyterhub/nbgitpuller/pull/345) ([@consideRatio](https://github.com/consideRatio), [@minrk](https://github.com/minrk), [@yuvipanda](https://github.com/yuvipanda)) + +#### Documentation improvements -### 1.1.1 +- Fix automatic merging link in README.md [#328](https://github.com/jupyterhub/nbgitpuller/pull/328) ([@mathbunnyru](https://github.com/mathbunnyru), [@manics](https://github.com/manics)) +- Add a FAQ entry for 'nbgitpuller link selecting profile options' [#322](https://github.com/jupyterhub/nbgitpuller/pull/322) ([@yuvipanda](https://github.com/yuvipanda), [@consideRatio](https://github.com/consideRatio), [@ryanlovett](https://github.com/ryanlovett), [@sgibson91](https://github.com/sgibson91)) + +#### Contributors to this release + +The following people contributed discussions, new ideas, code and documentation contributions, and review. +See [our definition of contributors](https://github-activity.readthedocs.io/en/latest/#how-does-this-tool-define-contributions-in-the-reports). + +([GitHub contributors page for this release](https://github.com/jupyterhub/nbgitpuller/graphs/contributors?from=2023-08-07&to=2024-03-29&type=c)) + +@balajialg ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fnbgitpuller+involves%3Abalajialg+updated%3A2023-08-07..2024-03-29&type=Issues)) | @consideRatio ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fnbgitpuller+involves%3AconsideRatio+updated%3A2023-08-07..2024-03-29&type=Issues)) | @fomightez ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fnbgitpuller+involves%3Afomightez+updated%3A2023-08-07..2024-03-29&type=Issues)) | @jtpio ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fnbgitpuller+involves%3Ajtpio+updated%3A2023-08-07..2024-03-29&type=Issues)) | @manics ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fnbgitpuller+involves%3Amanics+updated%3A2023-08-07..2024-03-29&type=Issues)) | @mathbunnyru ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fnbgitpuller+involves%3Amathbunnyru+updated%3A2023-08-07..2024-03-29&type=Issues)) | @minrk ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fnbgitpuller+involves%3Aminrk+updated%3A2023-08-07..2024-03-29&type=Issues)) | @ryanlovett ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fnbgitpuller+involves%3Aryanlovett+updated%3A2023-08-07..2024-03-29&type=Issues)) | @sgibson91 ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fnbgitpuller+involves%3Asgibson91+updated%3A2023-08-07..2024-03-29&type=Issues)) | @yuvipanda ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fnbgitpuller+involves%3Ayuvipanda+updated%3A2023-08-07..2024-03-29&type=Issues)) + +### 1.2.0 - 2023-08-07 + +([full changelog](https://github.com/jupyterhub/nbgitpuller/compare/1.1.1...1.2.0)) + +#### Enhancements made + +- Depend on jupyter-server only, compatibility with jupyter server >= 2, notebook < 7 [#240](https://github.com/jupyterhub/nbgitpuller/pull/240) ([@manics](https://github.com/manics), [@yuvipanda](https://github.com/yuvipanda), [@consideRatio](https://github.com/consideRatio), [@akhmerov](https://github.com/akhmerov), [@minrk](https://github.com/minrk), [@jtpio](https://github.com/jtpio)) + +#### Bugs fixed + +- fix handling of deleted-but-not-staged files with git 2.40 [#302](https://github.com/jupyterhub/nbgitpuller/pull/302) ([@minrk](https://github.com/minrk), [@consideRatio](https://github.com/consideRatio), [@yuvipanda](https://github.com/yuvipanda)) + +#### Maintenance and upkeep improvements + +- avoid deprecation warnings in test_api teardown [#301](https://github.com/jupyterhub/nbgitpuller/pull/301) ([@minrk](https://github.com/minrk), [@consideRatio](https://github.com/consideRatio), [@yuvipanda](https://github.com/yuvipanda)) +- redirect gh-pages to readthedocs [#298](https://github.com/jupyterhub/nbgitpuller/pull/298) ([@minrk](https://github.com/minrk), [@consideRatio](https://github.com/consideRatio)) +- migrate docs to RTD [#297](https://github.com/jupyterhub/nbgitpuller/pull/297) ([@minrk](https://github.com/minrk), [@consideRatio](https://github.com/consideRatio), [@yuvipanda](https://github.com/yuvipanda), [@frankier](https://github.com/frankier)) +- Bootstrap pre-commit config, add dependabot config, test py311 [#288](https://github.com/jupyterhub/nbgitpuller/pull/288) ([@consideRatio](https://github.com/consideRatio), [@yuvipanda](https://github.com/yuvipanda)) +- Fix test_exception_branch_exists [#287](https://github.com/jupyterhub/nbgitpuller/pull/287) ([@a3626a](https://github.com/a3626a), [@minrk](https://github.com/minrk), [@yuvipanda](https://github.com/yuvipanda)) +- Make tests work with different default branch, or different locale [#284](https://github.com/jupyterhub/nbgitpuller/pull/284) ([@jdmansour](https://github.com/jdmansour), [@yuvipanda](https://github.com/yuvipanda)) + +#### Documentation improvements + +- Link generator - support generating links for named servers [#309](https://github.com/jupyterhub/nbgitpuller/pull/309) ([@Snozzberries](https://github.com/Snozzberries), [@consideRatio](https://github.com/consideRatio)) +- Update README.md [#306](https://github.com/jupyterhub/nbgitpuller/pull/306) ([@Snozzberries](https://github.com/Snozzberries), [@yuvipanda](https://github.com/yuvipanda)) + +#### Continuous integration improvements + +- dependabot: monthly updates of github actions [#299](https://github.com/jupyterhub/nbgitpuller/pull/299) ([@consideRatio](https://github.com/consideRatio)) +- ci: relocate dependabot.yaml to correct location [#294](https://github.com/jupyterhub/nbgitpuller/pull/294) ([@consideRatio](https://github.com/consideRatio), [@yuvipanda](https://github.com/yuvipanda)) + +#### Contributors to this release + +The following people contributed discussions, new ideas, code and documentation contributions, and review. +See [our definition of contributors](https://github-activity.readthedocs.io/en/latest/#how-does-this-tool-define-contributions-in-the-reports). + +([GitHub contributors page for this release](https://github.com/jupyterhub/nbgitpuller/graphs/contributors?from=2022-11-08&to=2023-08-07&type=c)) + +@a3626a ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fnbgitpuller+involves%3Aa3626a+updated%3A2022-11-08..2023-08-07&type=Issues)) | @akhmerov ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fnbgitpuller+involves%3Aakhmerov+updated%3A2022-11-08..2023-08-07&type=Issues)) | @albertmichaelj ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fnbgitpuller+involves%3Aalbertmichaelj+updated%3A2022-11-08..2023-08-07&type=Issues)) | @balajialg ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fnbgitpuller+involves%3Abalajialg+updated%3A2022-11-08..2023-08-07&type=Issues)) | @consideRatio ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fnbgitpuller+involves%3AconsideRatio+updated%3A2022-11-08..2023-08-07&type=Issues)) | @frankier ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fnbgitpuller+involves%3Afrankier+updated%3A2022-11-08..2023-08-07&type=Issues)) | @jdmansour ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fnbgitpuller+involves%3Ajdmansour+updated%3A2022-11-08..2023-08-07&type=Issues)) | @jtpio ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fnbgitpuller+involves%3Ajtpio+updated%3A2022-11-08..2023-08-07&type=Issues)) | @manics ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fnbgitpuller+involves%3Amanics+updated%3A2022-11-08..2023-08-07&type=Issues)) | @minrk ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fnbgitpuller+involves%3Aminrk+updated%3A2022-11-08..2023-08-07&type=Issues)) | @Snozzberries ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fnbgitpuller+involves%3ASnozzberries+updated%3A2022-11-08..2023-08-07&type=Issues)) | @yuvipanda ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fnbgitpuller+involves%3Ayuvipanda+updated%3A2022-11-08..2023-08-07&type=Issues)) + +## 1.1 + +### 1.1.1 - 2022-11-08 ([full changelog](https://github.com/jupyterhub/nbgitpuller/compare/1.1.0...1.1.1)) @@ -13,7 +84,7 @@ - Deal with modify/delete conflicts [#269](https://github.com/jupyterhub/nbgitpuller/pull/269) ([@jdmansour](https://github.com/jdmansour)) - Fix regression: can't reset some files anymore [#264](https://github.com/jupyterhub/nbgitpuller/pull/264) ([@jdmansour](https://github.com/jdmansour)) -#### Other merged PRs +#### Maintenance and upkeep improvements - Modernize JS a little [#273](https://github.com/jupyterhub/nbgitpuller/pull/273) ([@yuvipanda](https://github.com/yuvipanda)) - Remove jquery dependency [#272](https://github.com/jupyterhub/nbgitpuller/pull/272) ([@yuvipanda](https://github.com/yuvipanda)) @@ -29,7 +100,7 @@ [@balajialg](https://github.com/search?q=repo%3Ajupyterhub%2Fnbgitpuller+involves%3Abalajialg+updated%3A2022-03-19..2022-11-08&type=Issues) | [@consideRatio](https://github.com/search?q=repo%3Ajupyterhub%2Fnbgitpuller+involves%3AconsideRatio+updated%3A2022-03-19..2022-11-08&type=Issues) | [@farcila](https://github.com/search?q=repo%3Ajupyterhub%2Fnbgitpuller+involves%3Afarcila+updated%3A2022-03-19..2022-11-08&type=Issues) | [@jdmansour](https://github.com/search?q=repo%3Ajupyterhub%2Fnbgitpuller+involves%3Ajdmansour+updated%3A2022-03-19..2022-11-08&type=Issues) | [@yuvipanda](https://github.com/search?q=repo%3Ajupyterhub%2Fnbgitpuller+involves%3Ayuvipanda+updated%3A2022-03-19..2022-11-08&type=Issues) -### 1.1.0 +### 1.1.0 - 2022-03-19 ([full changelog](https://github.com/jupyterhub/nbgitpuller/compare/1.0.2...1.1.0)) @@ -68,6 +139,8 @@ [@akhmerov](https://github.com/search?q=repo%3Ajupyterhub%2Fnbgitpuller+involves%3Aakhmerov+updated%3A2021-09-02..2022-03-18&type=Issues) | [@brian-rose](https://github.com/search?q=repo%3Ajupyterhub%2Fnbgitpuller+involves%3Abrian-rose+updated%3A2021-09-02..2022-03-18&type=Issues) | [@choldgraf](https://github.com/search?q=repo%3Ajupyterhub%2Fnbgitpuller+involves%3Acholdgraf+updated%3A2021-09-02..2022-03-18&type=Issues) | [@consideRatio](https://github.com/search?q=repo%3Ajupyterhub%2Fnbgitpuller+involves%3AconsideRatio+updated%3A2021-09-02..2022-03-18&type=Issues) | [@jameshowison](https://github.com/search?q=repo%3Ajupyterhub%2Fnbgitpuller+involves%3Ajameshowison+updated%3A2021-09-02..2022-03-18&type=Issues) | [@jdmansour](https://github.com/search?q=repo%3Ajupyterhub%2Fnbgitpuller+involves%3Ajdmansour+updated%3A2021-09-02..2022-03-18&type=Issues) | [@manics](https://github.com/search?q=repo%3Ajupyterhub%2Fnbgitpuller+involves%3Amanics+updated%3A2021-09-02..2022-03-18&type=Issues) | [@ryanlovett](https://github.com/search?q=repo%3Ajupyterhub%2Fnbgitpuller+involves%3Aryanlovett+updated%3A2021-09-02..2022-03-18&type=Issues) | [@welcome](https://github.com/search?q=repo%3Ajupyterhub%2Fnbgitpuller+involves%3Awelcome+updated%3A2021-09-02..2022-03-18&type=Issues) | [@yuvipanda](https://github.com/search?q=repo%3Ajupyterhub%2Fnbgitpuller+involves%3Ayuvipanda+updated%3A2021-09-02..2022-03-18&type=Issues) | [@Zsailer](https://github.com/search?q=repo%3Ajupyterhub%2Fnbgitpuller+involves%3AZsailer+updated%3A2021-09-02..2022-03-18&type=Issues) +## 1.0 + ### 1.0.2 - 2021-09-03 A release to fix an issue that stopped us from publishing nbgitpuller on diff --git a/README.md b/README.md index da2a2575..7326b4fd 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # [nbgitpuller](https://github.com/jupyterhub/nbgitpuller) [![GitHub Workflow Status - Test](https://img.shields.io/github/workflow/status/jupyterhub/nbgitpuller/Tests?logo=github&label=tests)](https://github.com/jupyterhub/nbgitpuller/actions) -[![CircleCI build status](https://img.shields.io/circleci/build/github/jupyterhub/nbgitpuller?logo=circleci&label=docs)](https://circleci.com/gh/jupyterhub/nbgitpuller) +[![Documentation Status](https://readthedocs.org/projects/nbgitpuller/badge/?version=latest)](https://nbgitpuller.readthedocs.io/en/latest/?badge=latest) [![](https://img.shields.io/pypi/v/nbgitpuller.svg?logo=pypi)](https://pypi.python.org/pypi/nbgitpuller) [![GitHub](https://img.shields.io/badge/issue_tracking-github-blue?logo=github)](https://github.com/jupyterhub/nbgitpuller/issues) [![Discourse](https://img.shields.io/badge/help_forum-discourse-blue?logo=discourse)](https://discourse.jupyter.org/c/jupyterhub) @@ -9,11 +9,11 @@ `nbgitpuller` lets you distribute content in a git repository to your students by having them click a simple link. [Automatic -merging](https://jupyterhub.github.io/nbgitpuller/topic/automatic-merging.html) +merging](https://nbgitpuller.readthedocs.io/en/latest/topic/automatic-merging.html) ensures that your students are never exposed to `git` directly. It is primarily used with a JupyterHub, but can also work on students' local computers. -See [the documentation](https://jupyterhub.github.io/nbgitpuller) for more +See [the documentation](https://nbgitpuller.readthedocs.io) for more information. ## Installation @@ -24,10 +24,12 @@ pip install nbgitpuller ## Example -This example shows how to use the [nbgitpuller link generator](https://jupyterhub.github.io/nbgitpuller/link) +This example shows how to use the [nbgitpuller link generator] to create an nbgitpuller link, which a user then clicks. -1. The [nbgitpuller link generator GUI](https://jupyterhub.github.io/nbgitpuller/link) is used to create a +[nbgitpuller link generator]: https://nbgitpuller.readthedocs.io/en/latest/link.html + +1. The nbgitpuller link generator GUI is used to create a link. ![](https://raw.githubusercontent.com/jupyterhub/nbgitpuller/9f380a933335f0f069b6e2f9965ed78c3abcce7a/docs/_static/nbgitpuller-link-generator.png) diff --git a/RELEASE.md b/RELEASE.md index c8488f03..a51fe33b 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -1,67 +1,60 @@ # How to make a release -`nbgitpuller` is a package available on -[PyPI](https://pypi.org/project/nbgitpuller/) and -[conda-forge](https://anaconda.org/conda-forge/nbgitpuller). -These are instructions on how to make a release on PyPI. -The PyPI release is done automatically by TravisCI when a tag is pushed. +`nbgitpuller` is a package available on [PyPI] and [conda-forge]. + +These are the instructions on how to make a release. + +## Pre-requisites + +- Push rights to this GitHub repository ## Steps to make a release -1. Checkout main and make sure it is up to date. +1. Create a PR updating `CHANGELOG.md` with [github-activity] and continue when + its merged. - ```shell - ORIGIN=${ORIGIN:-origin} # set to the canonical remote, e.g. 'upstream' if 'origin' is not the official repo - git checkout main - git fetch $ORIGIN main - git reset --hard $ORIGIN/main - # WARNING! This next command deletes any untracked files in the repo - git clean -xfd - ``` + Advice on this procedure can be found in [this team compass + issue](https://github.com/jupyterhub/team-compass/issues/563). -1. Set the `__version__` variable in - [`nbgitpuller/version.py`](nbgitpuller/version.py) - and make a commit. +2. Checkout main and make sure it is up to date. ```shell - git add nbgitpuller/version.py - VERSION=... # e.g. 1.2.3 - git commit -m "release $VERSION" + git checkout main + git fetch origin main + git reset --hard origin/main ``` -1. Reset the `__version__` variable in - [`nbgitpuller/version.py`](nbgitpuller/version.py) - to an incremented patch version with a `dev` element, then make a commit. +3. Update the version, make commits, and push a git tag with `tbump`. ```shell - git add nbgitpuller/version.py - git commit -m "back to dev" + pip install tbump ``` -1. Push your two commits to main. + `tbump` will ask for confirmation before doing anything. ```shell - # first push commits without a tags to ensure the - # commits comes through, because a tag can otherwise - # be pushed all alone without company of rejected - # commits, and we want have our tagged release coupled - # with a specific commit in main - git push $ORIGIN main + # Example versions to set: 1.0.0, 1.0.0b1 + VERSION= + tbump ${VERSION} ``` -1. Create a git tag for the pushed release commit and push it. - - ```shell - git tag -a $VERSION -m $VERSION HEAD~1 + Following this, the [CI system] will build and publish a release. - # then verify you tagged the right commit - git log +4. Reset the version back to dev, e.g. `1.0.1.dev` after releasing `1.0.0`. - # then push it - git push $ORIGIN refs/tags/$VERSION + ```shell + # Example version to set: 1.0.1.dev + NEXT_VERSION= + tbump --no-tag ${NEXT_VERSION}.dev ``` -1. Following the release to PyPI, an automated PR should arrive to - [conda-forge/nbgitpuller-feedstock](https://github.com/conda-forge/nbgitpuller-feedstock), - check for the tests to succeed on this PR and then merge it to successfully - update the package for `conda` on the `conda-forge` channel. +5. Following the release to PyPI, an automated PR should arrive within 24 hours + to [conda-forge/nbgitpuller-feedstock] with instructions + on releasing to conda-forge. You are welcome to volunteer doing this, but + aren't required as part of making this release to PyPI. + +[github-activity]: https://github.com/executablebooks/github-activity +[pypi]: https://pypi.org/project/nbgitpuller/ +[conda-forge]: https://anaconda.org/conda-forge/nbgitpuller +[conda-forge/nbgitpuller-feedstock]: https://github.com/conda-forge/nbgitpuller-feedstock +[ci system]: https://github.com/jupyterhub/nbgitpuller/actions/workflows/release.yaml diff --git a/dev-requirements.txt b/dev-requirements.txt index 03baf1b9..820cbabd 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -1,4 +1,6 @@ jupyter-packaging>=0.10 nbclassic +notebook>=5.5 +packaging pytest pytest-cov diff --git a/docs/_static/link_gen/link.js b/docs/_static/link_gen/link.js index f5e11e2d..3dd0dab7 100644 --- a/docs/_static/link_gen/link.js +++ b/docs/_static/link_gen/link.js @@ -1,5 +1,5 @@ // Pure function that generates an nbgitpuller URL -function generateRegularUrl(hubUrl, urlPath, repoUrl, branch) { +function generateRegularUrl(hubUrl, serverPath, urlPath, repoUrl, branch) { // assume hubUrl is a valid URL var url = new URL(hubUrl); @@ -17,7 +17,12 @@ function generateRegularUrl(hubUrl, urlPath, repoUrl, branch) { if (!url.pathname.endsWith('/')) { url.pathname += '/' } - url.pathname += 'hub/user-redirect/git-pull'; + + if (serverPath) { + url.pathname += 'hub/user-redirect/'+serverPath+'/git-pull'; + } else { + url.pathname += 'hub/user-redirect/git-pull'; + } return url.toString(); } @@ -160,6 +165,7 @@ function displayLink() { var contentRepoUrl = document.getElementById('content-repo').value; var contentRepoBranch = document.getElementById('content-branch').value; var filePath = document.getElementById('filepath').value; + var server = document.getElementById('server').value; var appName = form.querySelector('input[name="app"]:checked').value; var activeTab = document.querySelector(".nav-link.active").id; @@ -178,14 +184,14 @@ function displayLink() { if (activeTab === "tab-auth-default") { document.getElementById('default-link').value = generateRegularUrl( - hubUrl, urlPath, repoUrl, branch + hubUrl, server, urlPath, repoUrl, branch ); } else if (activeTab === "tab-auth-canvas"){ document.getElementById('canvas-link').value = generateCanvasUrl( hubUrl, urlPath, repoUrl, branch ); } else if (activeTab === "tab-auth-binder"){ - // FIXME: userName parsing using new URL(...) assumes a + // FIXME: userName parsing using new URL(...) assumes a // HTTP based repoUrl. Does it make sense to create a // BinderHub link for SSH URLs? Then let's fix this parsing. var userName = new URL(repoUrl).pathname.split('/')[1]; @@ -248,7 +254,7 @@ function render() { /** * Entry point */ -function main() { +function linkMain() { // Hook up any changes in form elements to call render() document.querySelectorAll('#linkgenerator input[type="radio"]').forEach( function (element) { @@ -265,16 +271,22 @@ function main() { // Activate tabs based on search parameters var params = new URL(window.location).searchParams; - if (params.get("tab")) { - if (params.get("tab") === "binder") { - $("#tab-auth-binder").click() - } else if (params.get("tab") === "canvas") { - $("#tab-auth-canvas").click() - } + switch(params.get("tab")) { + case "binder": + $("#tab-auth-binder").click(); + break; + case "canvas": + $("#tab-auth-canvas").click(); + break; } // Do an initial render, to make sure our disabled / enabled properties are correctly set render(); } -window.onload = main; +function copyLink(elementId) { + var copyText = document.getElementById(elementId); + copyText.select(); + copyText.setSelectionRange(0, copyText.value.length); + navigator.clipboard.writeText(copyText.value); +} diff --git a/docs/conf.py b/docs/conf.py index baa0be9a..c21428a1 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -53,7 +53,7 @@ def setup(app): # # This is also used if you do content translation via gettext catalogs. # Usually you set "language" from the command line for these cases. -language = None +language = "en" # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. diff --git a/docs/contributing.md b/docs/contributing.md index 85097105..c8acf96c 100644 --- a/docs/contributing.md +++ b/docs/contributing.md @@ -3,7 +3,7 @@ ## Setup nbgitpuller is a jupyter extension that works with both the -[classic Notebook Server](https://jupyter-notebook.readthedocs.io/en/stable/extending/handlers.html), +[classic Notebook Server](https://nbclassic.readthedocs.io/en/latest/extending/handlers.html), and the newer [Jupyter Server](https://jupyter-server.readthedocs.io/en/latest/operators/configuring-extensions.html). Hence, nbgitpuller can be developed locally without needing a JupyterHub. diff --git a/docs/faq.md b/docs/faq.md new file mode 100644 index 00000000..2cec8c76 --- /dev/null +++ b/docs/faq.md @@ -0,0 +1,23 @@ +# Frequently asked questions + +## Can I automatically tell JupyterHub what kind of server to start (node size, profile name, etc) as part of my nbgitpuller link? + +You can use Kubespawner's profile_list or ProfileSpawner to allow your +end users to choose the resources (memory, cpu, GPUs, etc) they want before +starting their server. Wouldn't it be nice if this information could be +embedded in the nbgitpuller link, so this (often confusing) choice is made +for your students? + +While it would indeed be very nice, this is currently not easy for two +reasons: + +1. nbgitpuller is a Jupyter Server extension, and only runs _after_ the server + is started. It knows nothing about JupyterHub. So it can not influence the + options JupyterHub uses to start the server. +2. There is UX complexity in what happens if the user clicks an nbgitpuller + link when a server is _already_ running, but with a different set of resource + requests / profile options. Do we shut that existing one down? Just error? Do + nothing? Many valid options, but takes a bunch of work. + +So while this workflow _is_ possible, it would most likely be done at the +JupyterHub level to make it possible, rather than in nbgitpuller diff --git a/docs/index.md b/docs/index.md index 799a7bab..90013a35 100644 --- a/docs/index.md +++ b/docs/index.md @@ -10,9 +10,7 @@ It is commonly used to distribute content to multiple users of a JupyterHub, tho Here's an example of `nbgitpuller` in action: -1. The [nbgitpuller link - generator](https://jupyterhub.github.io/nbgitpuller/link) is used to create a - link. +1. The [nbgitpuller link generator](link) is used to create a link. ```{image} _static/nbgitpuller-link-generator.png @@ -71,5 +69,6 @@ contributing topic/automatic-merging topic/url-options topic/repo-best-practices +faq link ``` diff --git a/docs/link.rst b/docs/link.rst index c7516897..969caf67 100644 --- a/docs/link.rst +++ b/docs/link.rst @@ -12,42 +12,51 @@ Use the following form to create your own ``nbgitpuller`` links. .. raw:: html -
+
-
-
- -
-
- -
-
- -
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
- -
@@ -171,10 +180,26 @@ Use the following form to create your own ``nbgitpuller`` links.
+
+ +
+ + + Use for specific named server Jupyter server instance. + +
+
+



+ + **Pre-populating some fields in the link generator** @@ -183,19 +208,19 @@ users to create their own links. To do so, use the following URL parameters **when accessing this page**: * ``hub`` is the URL of a JupyterHub -* ``repo`` is the URL of a github repository to which you're linking +* ``repo`` is the URL of a GitHub repository to which you're linking * ``branch`` is the branch you wish to pull from the Repository For example, the following URL will pre-populate the form with the UC Berkeley DataHub as the JupyterHub:: - https://jupyterhub.github.io/nbgitpuller/link?hub=https://datahub.berkeley.edu + https://nbgitpuller.readthedocs.io/link.html?hub=https://datahub.berkeley.edu **Activating a tab when someone lands on this page** You can also activate one of the tabs in the form above by default when a user lands -on this page. To do so, use the ``tab=`` REST parameter. Here are the possible values: +on this page. To do so, use the ``tab=`` query parameter. Here are the possible values: * ``?tab=binder`` - activates the Binder tab -* ``?tab=canvas`` - activates the Canvas tab. +* ``?tab=canvas`` - activates the Canvas tab diff --git a/docs/topic/url-options.rst b/docs/topic/url-options.rst index 200d3be1..7a6a0f5b 100644 --- a/docs/topic/url-options.rst +++ b/docs/topic/url-options.rst @@ -7,7 +7,7 @@ Options in an nbgitpuller URL .. note:: If you just want to generate an nbgitpuller link, we highly - recommend just using the `link generator `_ + recommend just using the :doc:`link generator <../link>` Most aspects of the nbgitpuller student experience can be configured with various options in the nbgitpuller URL. This page documents @@ -41,12 +41,12 @@ the file to be opened in. the local repository directory too, otherwise nbgitpuller can not find the file. - For example, if the repository you are cloning is + For example, if the repository you are cloning is ``https://github.com/my-user/my-repository``, and the file you want your students to see is ``index.ipynb``, then ```` should be ``my-repository/index.ipynb``, **not** ``index.ipynb``. - The `link generator `_ + The :doc:`link generator <../link>` takes care of all of this for you, so it is recommended to use that. diff --git a/nbgitpuller/__init__.py b/nbgitpuller/__init__.py index 18da0ccb..47973f96 100644 --- a/nbgitpuller/__init__.py +++ b/nbgitpuller/__init__.py @@ -1,7 +1,6 @@ from .version import __version__ # noqa -from .handlers import SyncHandler, UIHandler, LegacyInteractRedirectHandler, LegacyGitSyncRedirectHandler from .pull import GitPuller # noqa -from notebook.utils import url_path_join +from jupyter_server.utils import url_path_join from tornado.web import StaticFileHandler import os @@ -33,6 +32,19 @@ def _load_jupyter_server_extension(app): - notebook: https://jupyter-notebook.readthedocs.io/en/stable/examples/Notebook/Distributing%20Jupyter%20Extensions%20as%20Python%20Packages.html#Example---Server-extension - jupyter_server: https://jupyter-server.readthedocs.io/en/latest/developers/extensions.html """ + # identify base handler by app class + # must do this before importing from .handlers + from ._compat import get_base_handler + + get_base_handler(app) + + from .handlers import ( + SyncHandler, + UIHandler, + LegacyInteractRedirectHandler, + LegacyGitSyncRedirectHandler, + ) + web_app = app.web_app base_url = url_path_join(web_app.settings['base_url'], 'git-pull') handlers = [ diff --git a/nbgitpuller/_compat.py b/nbgitpuller/_compat.py new file mode 100644 index 00000000..036967c9 --- /dev/null +++ b/nbgitpuller/_compat.py @@ -0,0 +1,40 @@ +"""Import base Handler classes from Jupyter Server or Notebook + +Must be called before importing .handlers to ensure the correct base classes +""" +import warnings + +_JupyterHandler = None + + +def get_base_handler(app=None): + """Get the base JupyterHandler class to use + + Inferred from app class (either jupyter_server or notebook app) + """ + global _JupyterHandler + if _JupyterHandler is not None: + return _JupyterHandler + if app is None: + warnings.warn( + "Guessing base JupyterHandler class. Specify an app to ensure the right JupyterHandler is used.", + stacklevel=2, + ) + from jupyter_server.base.handlers import JupyterHandler + return JupyterHandler + + top_modules = {cls.__module__.split(".", 1)[0] for cls in app.__class__.mro()} + if "jupyter_server" in top_modules: + from jupyter_server.base.handlers import JupyterHandler + + _JupyterHandler = JupyterHandler + return _JupyterHandler + if "notebook" in top_modules: + from notebook.base.handlers import IPythonHandler + + _JupyterHandler = IPythonHandler + return _JupyterHandler + + warnings.warn(f"Failed to detect base JupyterHandler class for {app}.", stacklevel=2) + from jupyter_server.base.handlers import JupyterHandler + return JupyterHandler diff --git a/nbgitpuller/handlers.py b/nbgitpuller/handlers.py index 1bf38d63..544e8a43 100644 --- a/nbgitpuller/handlers.py +++ b/nbgitpuller/handlers.py @@ -2,7 +2,6 @@ import traceback import urllib.parse -from notebook.base.handlers import IPythonHandler import threading import json import os @@ -11,6 +10,9 @@ from .pull import GitPuller from .version import __version__ +from ._compat import get_base_handler + +JupyterHandler = get_base_handler() jinja_env = jinja2.Environment(loader=jinja2.FileSystemLoader( @@ -18,7 +20,7 @@ ), ) -class SyncHandler(IPythonHandler): +class SyncHandler(JupyterHandler): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -27,6 +29,12 @@ def __init__(self, *args, **kwargs): if 'git_lock' not in self.settings: self.settings['git_lock'] = locks.Lock() + def get_login_url(self): + # raise on failed auth, not redirect + # can't redirect EventStream to login + # same as Jupyter's APIHandler + raise web.HTTPError(403) + @property def git_lock(self): return self.settings['git_lock'] @@ -146,7 +154,7 @@ def pull(): )) -class UIHandler(IPythonHandler): +class UIHandler(JupyterHandler): @web.authenticated async def get(self): app_env = os.getenv('NBGITPULLER_APP', default='notebook') @@ -160,8 +168,11 @@ async def get(self): self.get_argument('subPath', '.') app = self.get_argument('app', app_env) parent_reldir = os.getenv('NBGITPULLER_PARENTPATH', '') + # Remove trailing slashes before determining default targetPath + # Otherwise we end up with targetpath being `.`, which always exists (given it is current + # working directory) and we end up with weird failures targetpath = self.get_argument('targetpath', None) or \ - self.get_argument('targetPath', repo.split('/')[-1]) + self.get_argument('targetPath', repo.rstrip('/').split('/')[-1]) if urlPath: path = urlPath @@ -201,7 +212,7 @@ def combine_query_string(self, targetpath): return targetpath -class LegacyGitSyncRedirectHandler(IPythonHandler): +class LegacyGitSyncRedirectHandler(JupyterHandler): """ The /git-pull endpoint was previously exposed /git-sync. @@ -217,7 +228,7 @@ async def get(self): self.redirect(new_url) -class LegacyInteractRedirectHandler(IPythonHandler): +class LegacyInteractRedirectHandler(JupyterHandler): """ The /git-pull endpoint was previously exposed /interact. diff --git a/nbgitpuller/pull.py b/nbgitpuller/pull.py index dd085e64..f5a7dab1 100644 --- a/nbgitpuller/pull.py +++ b/nbgitpuller/pull.py @@ -170,8 +170,18 @@ def reset_deleted_files(self): upstream_deleted = self.find_upstream_changed('D') for filename in deleted_files: - # Filter out empty lines, and files that were deleted in the remote - if filename and filename not in upstream_deleted: + if not filename: + # filter out empty lines + continue + + if filename in upstream_deleted: + # deleted in _both_, avoid conflict with git 2.40 by checking it out + # even though it's just about to be deleted + yield from execute_cmd( + ['git', 'checkout', 'HEAD', '--', filename], cwd=self.repo_dir + ) + else: + # not deleted in upstream, restore with checkout yield from execute_cmd(['git', 'checkout', 'origin/{}'.format(self.branch_name), '--', filename], cwd=self.repo_dir) def repo_is_dirty(self): diff --git a/nbgitpuller/static/js/gitsync.js b/nbgitpuller/static/js/gitsync.js index e441d8a7..94d800a4 100644 --- a/nbgitpuller/static/js/gitsync.js +++ b/nbgitpuller/static/js/gitsync.js @@ -1,5 +1,5 @@ export class GitSync { - constructor(baseUrl, repo, branch, depth, targetpath, path) { + constructor(baseUrl, repo, branch, depth, targetpath, path, xsrf) { // Class that talks to the API backend & emits events as appropriate this.baseUrl = baseUrl; this.repo = repo; @@ -7,6 +7,7 @@ export class GitSync { this.depth = depth; this.targetpath = targetpath; this.redirectUrl = baseUrl + path; + this._xsrf = xsrf; this.callbacks = {}; } @@ -30,6 +31,7 @@ export class GitSync { start() { // Start git pulling handled by SyncHandler, declared in handlers.py let syncUrlParams = new URLSearchParams({ + _xsrf: this._xsrf, repo: this.repo, targetpath: this.targetpath }); diff --git a/nbgitpuller/static/js/index.js b/nbgitpuller/static/js/index.js index 02c31634..5e35f1aa 100644 --- a/nbgitpuller/static/js/index.js +++ b/nbgitpuller/static/js/index.js @@ -21,7 +21,8 @@ const gs = new GitSync( getBodyData('branch'), getBodyData('depth'), getBodyData('targetpath'), - getBodyData('path') + getBodyData('path'), + getBodyData('xsrf'), ); const gsv = new GitSyncView( diff --git a/nbgitpuller/templates/status.html b/nbgitpuller/templates/status.html index 2b969f59..fd19756e 100644 --- a/nbgitpuller/templates/status.html +++ b/nbgitpuller/templates/status.html @@ -5,6 +5,7 @@ data-base-url="{{ base_url | urlencode }}" data-repo="{{ repo | urlencode }}" data-path="{{ path | urlencode }}" +data-xsrf="{{ xsrf_token | urlencode }}" {% if branch %}data-branch="{{ branch | urlencode }}"{% endif %} {% if depth %}data-depth="{{ depth | urlencode }}"{% endif %} data-targetpath="{{ targetpath | urlencode }}" diff --git a/nbgitpuller/version.py b/nbgitpuller/version.py index 62e16d16..58596cf8 100644 --- a/nbgitpuller/version.py +++ b/nbgitpuller/version.py @@ -1,2 +1,4 @@ -""""The nbgitpuller PyPI package SemVer version.""" -__version__ = '1.1.2dev' +# __version__ should be updated using tbump, based on configuration in +# pyproject.toml, according to instructions in RELEASE.md. +# +__version__ = "1.2.2.dev" diff --git a/pyproject.toml b/pyproject.toml index 3c871fce..0854bf16 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -40,4 +40,50 @@ target_version = [ "py39", "py310", "py311", + "py312", ] + + +# pytest is used for running Python based tests +# +# ref: https://docs.pytest.org/en/stable/ +# +[tool.pytest.ini_options] +addopts = "--verbose --color=yes --durations=10 --cov nbgitpuller" +asyncio_mode = "auto" +testpaths = ["tests"] +markers = [ + "jupyter_server: configure the jupyter_server fixture" +] + + +# tbump is used to simplify and standardize the release process when updating +# the version, making a git commit and tag, and pushing changes. +# +# ref: https://github.com/your-tools/tbump#readme +# +[tool.tbump] +github_url = "https://github.com/jupyterhub/nbgitpuller" + +[tool.tbump.version] +current = "1.2.2.dev" +regex = ''' + (?P\d+) + \. + (?P\d+) + \. + (?P\d+) + (?P
((a|b|rc)\d+)|)
+    \.?
+    (?P(?<=\.)dev\d*|)
+'''
+
+[tool.tbump.git]
+message_template = "Bump to {new_version}"
+tag_template = "{new_version}"
+
+[[tool.tbump.file]]
+src = "nbgitpuller/version.py"
+
+[[tool.tbump.file]]
+src = "setup.py"
diff --git a/setup.py b/setup.py
index 3efef420..8eec22bb 100644
--- a/setup.py
+++ b/setup.py
@@ -1,6 +1,5 @@
 from jupyter_packaging import wrap_installers, npm_builder
 from setuptools import find_packages, setup
-from distutils.util import convert_path
 import os.path
 
 HERE = os.path.abspath(os.path.dirname(__file__))
@@ -17,28 +16,21 @@
     pre_develop=jsdeps, pre_dist=jsdeps,
     ensured_targets=jstargets)
 
-# Imports __version__, reference: https://stackoverflow.com/a/24517154/2220152
-ns = {}
-ver_path = convert_path('nbgitpuller/version.py')
-with open(ver_path) as ver_file:
-    exec(ver_file.read(), ns)
-__version__ = ns['__version__']
-
 setup(
     name='nbgitpuller',
-    version=__version__,
+    version="1.2.2.dev",
     url='https://github.com/jupyterhub/nbgitpuller',
     license='3-clause BSD',
     author='Peter Veerman, YuviPanda',
     author_email='peterkangveerman@gmail.com',
     cmdclass=cmdclass,
-    description='Notebook Extension to do one-way synchronization of git repositories',
+    description='Jupyter Extension to do one-way synchronization of git repositories',
     long_description=open('README.md').read(),
     long_description_content_type='text/markdown',
     packages=find_packages(),
     include_package_data=True,
     platforms='any',
-    install_requires=['notebook>=5.5.0', 'jupyter_server>=1.10.1', 'tornado'],
+    install_requires=['jupyter_server>=1.10.1', 'tornado'],
     data_files=[
         ('etc/jupyter/jupyter_server_config.d', ['nbgitpuller/etc/jupyter_server_config.d/nbgitpuller.json']),
         ('etc/jupyter/jupyter_notebook_config.d', ['nbgitpuller/etc/jupyter_notebook_config.d/nbgitpuller.json'])
@@ -50,7 +42,7 @@
         ],
     },
     classifiers=[
-        'Development Status :: 4 - Beta',
+        'Development Status :: 5 - Production/Stable',
         'License :: OSI Approved :: BSD License',
         'Operating System :: POSIX',
         'Operating System :: MacOS',
diff --git a/tests/repohelpers.py b/tests/repohelpers.py
index 2052bcf2..98bfd110 100644
--- a/tests/repohelpers.py
+++ b/tests/repohelpers.py
@@ -7,6 +7,7 @@
 import subprocess as sp
 from uuid import uuid4
 
+from packaging.version import Version as V
 from nbgitpuller import GitPuller
 
 
@@ -18,7 +19,14 @@ def __init__(self, path=None):
 
     def __enter__(self):
         os.makedirs(self.path, exist_ok=True)
-        self.git('init', '--bare', '--initial-branch=master')
+
+        # --initial-branch added in git 2.28
+        git_version = self.git("--version").split()[-1]
+        if V(git_version) >= V("2.28"):
+            extra_args = ('--initial-branch=master',)
+        else:
+            extra_args = ()
+        self.git('init', '--bare', *extra_args)
         return self
 
     def __exit__(self, *args):
diff --git a/tests/test_api.py b/tests/test_api.py
index 3622bb6c..edb78170 100644
--- a/tests/test_api.py
+++ b/tests/test_api.py
@@ -1,9 +1,10 @@
 import os
 from http.client import HTTPConnection
 import subprocess
-from time import sleep
-from urllib.parse import quote
+import time
+from urllib.parse import urlencode
 from uuid import uuid4
+import notebook
 import pytest
 
 from repohelpers import Pusher, Remote
@@ -12,127 +13,172 @@
 
 
 def request_api(params, host='localhost'):
+    query_args = {"token": "secret"}
+    query_args.update(params)
+    query = urlencode(query_args)
+    url = f'/git-pull/api?{query}'
     h = HTTPConnection(host, PORT, 10)
-    query = '&'.join('{}={}'.format(k, quote(v)) for (k, v) in params.items())
-    url = '/git-pull/api?token=secret&{}'.format(query)
     h.request('GET', url)
     return h.getresponse()
 
+def wait_for_server(host='localhost', port=PORT, timeout=10):
+    """Wait for an HTTP server to be responsive"""
+    t = 0.1
+    deadline = time.monotonic() + timeout
+    while time.monotonic() < deadline:
+        try:
+            h = HTTPConnection(host, port, 10)
+            h.request("GET", "/")
+            r = h.getresponse()
+        except Exception as e:
+            print(f"Server not ready: {e}")
+            time.sleep(t)
+            t *= 2
+            t = min(t, 1)
+        else:
+            # success
+            return
+    assert False, f"Server never showed up at http://{host}:{port}"
 
-class TestNbGitPullerApi:
 
-    def setup(self):
-        self.jupyter_proc = None
+@pytest.fixture
+def jupyterdir(tmpdir):
+    path = tmpdir.join("jupyter")
+    path.mkdir()
+    return str(path)
 
-    def teardown(self):
-        if self.jupyter_proc:
-            self.jupyter_proc.kill()
 
-    def start_jupyter(self, jupyterdir, extraenv, backend_type):
-        env = os.environ.copy()
-        env.update(extraenv)
-        if "server" in backend_type:
-            command = [
-                'jupyter-server',
-                '--NotebookApp.token=secret',
-                '--port={}'.format(PORT),
-            ]
-        else:
-            command = [
-                'jupyter-notebook',
-                '--no-browser',
-                '--NotebookApp.token=secret',
-                '--port={}'.format(PORT),
-            ]
-        self.jupyter_proc = subprocess.Popen(command, cwd=jupyterdir, env=env)
-        sleep(2)
-
-    @pytest.mark.parametrize(
-        "backend_type",
-        [
-            ("jupyter-server"),
-            ("jupyter-notebook"),
-        ],
-    )
-    def test_clone_default(self, tmpdir, backend_type):
-        """
-        Tests use of 'repo' and 'branch' parameters.
-        """
-        jupyterdir = str(tmpdir)
-        self.start_jupyter(jupyterdir, {}, backend_type)
-
-        with Remote() as remote, Pusher(remote) as pusher:
-            pusher.push_file('README.md', 'Testing some content')
-            print(f'path: {remote.path}')
-            params = {
-                'repo': remote.path,
-                'branch': 'master',
-            }
-            r = request_api(params)
-            assert r.code == 200
-            s = r.read().decode()
-            print(s)
-            target_path = os.path.join(jupyterdir, os.path.basename(remote.path))
-            assert '--branch master' in s
-            assert f"Cloning into '{target_path}" in s
-            assert os.path.isdir(os.path.join(target_path, '.git'))
-
-    @pytest.mark.parametrize(
-        "backend_type",
-        [
-            ("jupyter-server"),
-            ("jupyter-notebook"),
-        ],
-    )
-    def test_clone_targetpath(self, tmpdir, backend_type):
-        """
-        Tests use of 'targetpath' parameter.
-        """
-        jupyterdir = str(tmpdir)
-        target = str(uuid4())
-        self.start_jupyter(jupyterdir, {}, backend_type)
-        with Remote() as remote, Pusher(remote) as pusher:
-            pusher.push_file('README.md', 'Testing some content')
-            params = {
-                'repo': remote.path,
-                'branch': 'master',
-                'targetpath': target,
-            }
-            r = request_api(params)
-            assert r.code == 200
-            s = r.read().decode()
-            print(s)
-            target_path = os.path.join(jupyterdir, target)
-            assert f"Cloning into '{target_path}" in s
-            assert os.path.isdir(os.path.join(target_path, '.git'))
-
-    @pytest.mark.parametrize(
-        "backend_type",
-        [
-            ("jupyter-server"),
-            ("jupyter-notebook"),
-        ],
-    )
-    def test_clone_parenttargetpath(self, tmpdir, backend_type):
-        """
-        Tests use of the NBGITPULLER_PARENTPATH environment variable.
-        """
-        jupyterdir = str(tmpdir)
-        parent = str(uuid4())
-        target = str(uuid4())
-        self.start_jupyter(jupyterdir, {'NBGITPULLER_PARENTPATH': parent}, backend_type)
-
-        with Remote() as remote, Pusher(remote) as pusher:
-            pusher.push_file('README.md', 'Testing some content')
-            params = {
-                'repo': remote.path,
-                'branch': 'master',
-                'targetpath': target,
-            }
-            r = request_api(params)
-            assert r.code == 200
-            s = r.read().decode()
-            print(s)
-            target_path = os.path.join(jupyterdir, parent, target)
-            assert f"Cloning into '{target_path}" in s
-            assert os.path.isdir(os.path.join(target_path, '.git'))
+@pytest.fixture(params=["jupyter-server", "jupyter-notebook"])
+def jupyter_server(request, tmpdir, jupyterdir):
+    # allow passing extra_env via @pytest.mark.jupyter_server(extra_env={"key": "value"})
+    if "jupyter_server" in request.keywords:
+        extra_env = request.keywords["jupyter_server"].kwargs.get("extra_env")
+    else:
+        extra_env = None
+
+    backend_type = request.param
+
+    env = os.environ.copy()
+    # avoid interacting with user configuration, state
+    env["JUPYTER_CONFIG_DIR"] = str(tmpdir / "dotjupyter")
+    env["JUPYTER_RUNTIME_DIR"] = str(tmpdir / "runjupyter")
+
+    if extra_env:
+        env.update(extra_env)
+
+    extension_command = ["jupyter", "server", "extension"]
+    if backend_type == "jupyter-server":
+        command = [
+            'jupyter-server',
+            '--ServerApp.token=secret',
+            '--port={}'.format(PORT),
+        ]
+    elif backend_type == "jupyter-notebook":
+        command = [
+            'jupyter-notebook',
+            '--no-browser',
+            '--NotebookApp.token=secret',
+            '--port={}'.format(PORT),
+        ]
+        # notebook <7 require "jupyter serverextension" instead of "jupyter
+        # server extension"
+        if notebook.version_info[0] < 7:
+            extension_command = ["jupyter", "serverextension"]
+    else:
+        raise ValueError(
+            f"backend_type must be 'jupyter-server' or 'jupyter-notebook' not {backend_type!r}"
+        )
+
+    # enable the extension
+    subprocess.check_call(extension_command + ["enable", "nbgitpuller"], env=env)
+
+    # launch the server
+    jupyter_proc = subprocess.Popen(command, cwd=jupyterdir, env=env)
+    wait_for_server()
+
+    with jupyter_proc:
+        yield jupyter_proc
+        jupyter_proc.terminate()
+
+
+def test_clone_default(jupyterdir, jupyter_server):
+    """
+    Tests use of 'repo' and 'branch' parameters.
+    """
+    with Remote() as remote, Pusher(remote) as pusher:
+        pusher.push_file('README.md', 'Testing some content')
+        print(f'path: {remote.path}')
+        params = {
+            'repo': remote.path,
+            'branch': 'master',
+        }
+        r = request_api(params)
+        assert r.code == 200
+        s = r.read().decode()
+        print(s)
+        target_path = os.path.join(jupyterdir, os.path.basename(remote.path))
+        assert '--branch master' in s
+        assert f"Cloning into '{target_path}" in s
+        assert os.path.isdir(os.path.join(target_path, '.git'))
+
+
+def test_clone_auth(jupyterdir, jupyter_server):
+    """
+    Tests use of 'repo' and 'branch' parameters.
+    """
+    with Remote() as remote, Pusher(remote) as pusher:
+        pusher.push_file('README.md', 'Testing some content')
+        print(f'path: {remote.path}')
+        params = {
+            'repo': remote.path,
+            'branch': 'master',
+            'token': 'wrong',
+        }
+        r = request_api(params)
+        # no token, redirect to login
+        assert r.code == 403
+
+
+def test_clone_targetpath(jupyterdir, jupyter_server):
+    """
+    Tests use of 'targetpath' parameter.
+    """
+    target = str(uuid4())
+    with Remote() as remote, Pusher(remote) as pusher:
+        pusher.push_file('README.md', 'Testing some content')
+        params = {
+            'repo': remote.path,
+            'branch': 'master',
+            'targetpath': target,
+        }
+        r = request_api(params)
+        assert r.code == 200
+        s = r.read().decode()
+        print(s)
+        target_path = os.path.join(jupyterdir, target)
+        assert f"Cloning into '{target_path}" in s
+        assert os.path.isdir(os.path.join(target_path, '.git'))
+
+
+@pytest.mark.jupyter_server(extra_env={'NBGITPULLER_PARENTPATH': "parent"})
+def test_clone_parenttargetpath(jupyterdir, jupyter_server):
+    """
+    Tests use of the NBGITPULLER_PARENTPATH environment variable.
+    """
+    parent = "parent"
+    target = str(uuid4())
+
+    with Remote() as remote, Pusher(remote) as pusher:
+        pusher.push_file('README.md', 'Testing some content')
+        params = {
+            'repo': remote.path,
+            'branch': 'master',
+            'targetpath': target,
+        }
+        r = request_api(params)
+        assert r.code == 200
+        s = r.read().decode()
+        print(s)
+        target_path = os.path.join(jupyterdir, parent, target)
+        assert f"Cloning into '{target_path}" in s
+        assert os.path.isdir(os.path.join(target_path, '.git'))
diff --git a/tests/test_gitpuller.py b/tests/test_gitpuller.py
index b4d742aa..02f7c2cc 100644
--- a/tests/test_gitpuller.py
+++ b/tests/test_gitpuller.py
@@ -88,13 +88,11 @@ def test_exception_branch_exists():
     with Remote() as remote, Pusher(remote) as pusher:
         pusher.push_file('README.md', '1')
         with Puller(remote) as puller:
-            orig_url = puller.gp.git_url
-            try:
+            with pytest.raises(sp.CalledProcessError):
+                orig_url = puller.gp.git_url
+                puller.gp.git_url = ""
                 puller.gp.branch_exists("wrong")
-            except Exception as e:
-                assert type(e) == ValueError
-            puller.gp.git_url = orig_url
-
+                puller.gp.git_url = orig_url
 
 def test_resolve_default_branch():
     with Remote() as remote, Pusher(remote) as pusher: