+
+
+
+
+
+
+
+
+
diff --git a/nbgitpuller/.circleci/config.yml b/nbgitpuller/.circleci/config.yml
new file mode 100644
index 0000000000000000000000000000000000000000..e12c7e5b4ecada159b155e80f42f8d6a7d9db281
--- /dev/null
+++ b/nbgitpuller/.circleci/config.yml
@@ -0,0 +1,65 @@
+version: 2.1
+jobs:
+ build_docs:
+ docker:
+ - image: circleci/python:3.6-stretch
+ steps:
+ - build_site
+ - store_artifacts:
+ path: docs/_build/html/
+ destination: html
+
+ push_docs:
+ docker:
+ - image: circleci/python:3.6-stretch
+ steps:
+ # Add deployment key fingerprint for CircleCI to use for a push
+ - add_ssh_keys:
+ fingerprints:
+ # The SSH key fingerprint
+ - "c5:70:b9:1b:9a:cf:e3:88:25:9f:33:8e:ee:09:76:9f"
+
+ - build_site
+
+ - run:
+ name: Pushing documentation to gh-pages
+ command: |
+ pip install --user ghp-import
+ ghp-import --no-jekyll --push --message "Update documentation [skip ci]" docs/_build/html
+
+workflows:
+ version: 2
+ default:
+ jobs:
+ - build_docs
+ - push_docs:
+ filters: # using regex filters requires the entire branch to match
+ branches:
+ only: # only branches matching the below regex filters will run
+ - main
+
+commands:
+ build_site:
+ description: "Build the site with sphinx"
+ steps:
+ # Get our data and merge with upstream
+ - run: sudo apt-get update
+ - checkout
+ # Python env
+ - run: echo "export PATH=~/.local/bin:$PATH" >> $BASH_ENV
+
+ - restore_cache:
+ keys:
+ - cache-pip
+ - run: pip install --user -r docs/doc-requirements.txt
+ - save_cache:
+ key: cache-pip
+ paths:
+ - ~/.cache/pip
+
+ # Build the docs
+ - run:
+ name: Build docs to store
+ command: |
+ cd docs
+ make html
diff --git a/nbgitpuller/.flake8 b/nbgitpuller/.flake8
new file mode 100644
index 0000000000000000000000000000000000000000..5619889bc961a8bf79b38255013f0209c99e9d37
--- /dev/null
+++ b/nbgitpuller/.flake8
@@ -0,0 +1,12 @@
+[flake8]
+# Ignore style and complexity
+# E: style errors
+# W: style warnings
+# C: complexity
+# E402: module level import not at top of file
+# I100: Import statements are in the wrong order
+# I101: Imported names are in the wrong order. Should be
+ignore = E, C, W, E402, I100, I101, D400
+exclude =
+ .cache,
+ .github
diff --git a/nbgitpuller/.github/workflows/publish.yml b/nbgitpuller/.github/workflows/publish.yml
new file mode 100644
index 0000000000000000000000000000000000000000..3dfe659f31093ac6b6eaf616974b6eb14fdf5be3
--- /dev/null
+++ b/nbgitpuller/.github/workflows/publish.yml
@@ -0,0 +1,35 @@
+# Build releases and (on tags) publish to PyPI
+name: Release
+
+# always build releases (to make sure wheel-building works)
+# but only publish to PyPI on tags
+on:
+ push:
+ pull_request:
+
+jobs:
+ build-release:
+ runs-on: ubuntu-20.04
+ steps:
+ - uses: actions/checkout@v2
+ - uses: actions/setup-python@v2
+ with:
+ python-version: 3.8
+
+ - name: install build package
+ run: |
+ pip install --upgrade pip
+ pip install build
+ pip freeze
+
+ - name: build release
+ run: |
+ python -m build --sdist --wheel .
+ ls -l dist
+
+ - name: publish to pypi
+ uses: pypa/gh-action-pypi-publish@v1.4.1
+ if: startsWith(github.ref, 'refs/tags/')
+ with:
+ user: __token__
+ password: ${{ secrets.pypi_password }}
diff --git a/nbgitpuller/.github/workflows/test.yml b/nbgitpuller/.github/workflows/test.yml
new file mode 100644
index 0000000000000000000000000000000000000000..89168d6186d2ac1a4fd1fb0ae5363097c1703622
--- /dev/null
+++ b/nbgitpuller/.github/workflows/test.yml
@@ -0,0 +1,54 @@
+# This is a GitHub workflow defining a set of jobs with a set of steps.
+# ref: https://docs.github.com/en/free-pro-team@latest/actions/reference/workflow-syntax-for-github-actions
+#
+name: Tests
+
+on:
+ pull_request:
+ push:
+ workflow_dispatch:
+
+jobs:
+ test:
+
+ runs-on: ubuntu-20.04
+ timeout-minutes: 10
+
+ steps:
+ - uses: actions/checkout@v2
+
+ - name: Run webpack to build static assets
+ run: |
+ npm install
+ npm run webpack
+
+ - name: Install Python
+ uses: actions/setup-python@v2
+ with:
+ python-version: '3.8'
+
+ # DISABLED: Since we don't pin our dependencies in dev-requirements.txt
+ # and only refresh the cache when it changes, we end up with a
+ # cache that remains for too long and cause failures. Due to
+ # this, it has been disabled.
+ #
+ # - name: Cache pip dependencies
+ # uses: actions/cache@v2
+ # with:
+ # path: ~/.cache/pip
+ # # Look to see if there is a cache hit for the corresponding requirements file
+ # key: ${{ runner.os }}-pip-${{ hashFiles('*requirements.txt') }}
+ # restore-keys: |
+ # ${{ runner.os }}-pip-
+
+ - name: Install dependencies
+ run: |
+ pip install -r dev-requirements.txt
+
+ - name: Run flake8 linter
+ run: flake8
+
+ - name: Run tests
+ run: |
+ pip install .
+ pytest --verbose --maxfail=2 --color=yes --cov nbgitpuller
diff --git a/nbgitpuller/.gitignore b/nbgitpuller/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..2d5462d17dbff0a79e42c299704250946ab36a57
--- /dev/null
+++ b/nbgitpuller/.gitignore
@@ -0,0 +1,24 @@
+*.pyc
+
+dist/
+build/
+*.egg-info/
+
+.tox/
+.coverage
+
+.DS_Store
+.cache/
+data8assets/
+.autopull_list
+summer/
+test-repo/
+venv/
+
+.ipynb_checkpoints
+docs/_build
+
+node_modules/
+package-lock.json
+
+nbgitpuller/static/dist
\ No newline at end of file
diff --git a/nbgitpuller/CHANGELOG.md b/nbgitpuller/CHANGELOG.md
new file mode 100644
index 0000000000000000000000000000000000000000..d93bbcda389a978c69fbaaf2905bb930f75755ed
--- /dev/null
+++ b/nbgitpuller/CHANGELOG.md
@@ -0,0 +1,138 @@
+## 0.10
+
+### 0.10.2 - 2021-08-25
+
+This is a critical security release, please upgrade to this and see [GHSA-mq5p-2mcr-m52j](https://github.com/jupyterhub/nbgitpuller/security/advisories/GHSA-mq5p-2mcr-m52j) more information.
+
+### 0.10.1 - 2021-06-24
+
+#### Bugs fixed
+
+- Added branch name back to command-line usage [#185](https://github.com/jupyterhub/nbgitpuller/pull/185) ([@sean-morris](https://github.com/sean-morris))
+
+#### Documentation improvements
+
+- Provide cleaner feedback for lint vs test failures [#181](https://github.com/jupyterhub/nbgitpuller/pull/181) ([@yuvipanda](https://github.com/yuvipanda))
+
+#### Continuous integration
+
+- Fix CI failures by disabling pip cache [#188](https://github.com/jupyterhub/nbgitpuller/pull/188) ([@consideRatio](https://github.com/consideRatio))
+
+#### Contributors to this release
+
+([GitHub contributors page for this release](https://github.com/jupyterhub/nbgitpuller/graphs/contributors?from=2021-06-09&to=2021-06-24&type=c))
+
+[@consideRatio](https://github.com/search?q=repo%3Ajupyterhub%2Fnbgitpuller+involves%3AconsideRatio+updated%3A2021-06-09..2021-06-24&type=Issues) | [@manics](https://github.com/search?q=repo%3Ajupyterhub%2Fnbgitpuller+involves%3Amanics+updated%3A2021-06-09..2021-06-24&type=Issues) | [@sean-morris](https://github.com/search?q=repo%3Ajupyterhub%2Fnbgitpuller+involves%3Asean-morris+updated%3A2021-06-09..2021-06-24&type=Issues) | [@yuvipanda](https://github.com/search?q=repo%3Ajupyterhub%2Fnbgitpuller+involves%3Ayuvipanda+updated%3A2021-06-09..2021-06-24&type=Issues)
+
+### 0.10.0 - 2021-06-09
+
+#### Enhancements made
+
+- UI: Branch input placeholder no longer suggests master branch [#180](https://github.com/jupyterhub/nbgitpuller/pull/180) ([@sean-morris](https://github.com/sean-morris))
+- Automatically detect default branch name [#179](https://github.com/jupyterhub/nbgitpuller/pull/179) ([@sean-morris](https://github.com/sean-morris))
+- Tell users about `main` vs `master` branches [#170](https://github.com/jupyterhub/nbgitpuller/pull/170) ([@yuvipanda](https://github.com/yuvipanda))
+- Support generating shiny links [#165](https://github.com/jupyterhub/nbgitpuller/pull/165) ([@yuvipanda](https://github.com/yuvipanda))
+
+#### Bugs fixed
+
+- Handle lack of trailing slashes in hub URLs [#173](https://github.com/jupyterhub/nbgitpuller/pull/173) ([@yuvipanda](https://github.com/yuvipanda))
+- Respect path component of JupyterHub url [#172](https://github.com/jupyterhub/nbgitpuller/pull/172) ([@yuvipanda](https://github.com/yuvipanda))
+- Parse ssh git URLs properly [#163](https://github.com/jupyterhub/nbgitpuller/pull/163) ([@yuvipanda](https://github.com/yuvipanda))
+- Fix failure to restore deleted files (use raw output of git ls-files to avoid quoting unicode) [#156](https://github.com/jupyterhub/nbgitpuller/pull/156) ([@manics](https://github.com/manics))
+- Compare current branch to target - don't assume already on target branch locally [#141](https://github.com/jupyterhub/nbgitpuller/pull/141) ([@danlester](https://github.com/danlester))
+
+#### Documentation improvements
+
+- Document restarting notebook process to see changes [#178](https://github.com/jupyterhub/nbgitpuller/pull/178) ([@yuvipanda](https://github.com/yuvipanda))
+- docs: update README.md badges [#175](https://github.com/jupyterhub/nbgitpuller/pull/175) ([@consideRatio](https://github.com/consideRatio))
+- Add best practices recommendation documentation [#169](https://github.com/jupyterhub/nbgitpuller/pull/169) ([@yuvipanda](https://github.com/yuvipanda))
+- Document how to do local development [#162](https://github.com/jupyterhub/nbgitpuller/pull/162) ([@yuvipanda](https://github.com/yuvipanda))
+- Add badges to README.md [#150](https://github.com/jupyterhub/nbgitpuller/pull/150) ([@consideRatio](https://github.com/consideRatio))
+
+#### Continuous Integration
+
+- CI: Replace Travis with GitHub workflow [#161](https://github.com/jupyterhub/nbgitpuller/pull/161) ([@manics](https://github.com/manics))
+- CI: stop triggering CircleCI on automated pushes to gh-pages [#151](https://github.com/jupyterhub/nbgitpuller/pull/151) ([@consideRatio](https://github.com/consideRatio))
+
+#### Contributors to this release
+
+([GitHub contributors page for this release](https://github.com/jupyterhub/nbgitpuller/graphs/contributors?from=2020-08-01&to=2021-06-09&type=c))
+
+[@albertmichaelj](https://github.com/search?q=repo%3Ajupyterhub%2Fnbgitpuller+involves%3Aalbertmichaelj+updated%3A2020-08-01..2021-06-09&type=Issues) | [@choldgraf](https://github.com/search?q=repo%3Ajupyterhub%2Fnbgitpuller+involves%3Acholdgraf+updated%3A2020-08-01..2021-06-09&type=Issues) | [@consideRatio](https://github.com/search?q=repo%3Ajupyterhub%2Fnbgitpuller+involves%3AconsideRatio+updated%3A2020-08-01..2021-06-09&type=Issues) | [@danlester](https://github.com/search?q=repo%3Ajupyterhub%2Fnbgitpuller+involves%3Adanlester+updated%3A2020-08-01..2021-06-09&type=Issues) | [@giumas](https://github.com/search?q=repo%3Ajupyterhub%2Fnbgitpuller+involves%3Agiumas+updated%3A2020-08-01..2021-06-09&type=Issues) | [@manics](https://github.com/search?q=repo%3Ajupyterhub%2Fnbgitpuller+involves%3Amanics+updated%3A2020-08-01..2021-06-09&type=Issues) | [@minrk](https://github.com/search?q=repo%3Ajupyterhub%2Fnbgitpuller+involves%3Aminrk+updated%3A2020-08-01..2021-06-09&type=Issues) | [@ryanlovett](https://github.com/search?q=repo%3Ajupyterhub%2Fnbgitpuller+involves%3Aryanlovett+updated%3A2020-08-01..2021-06-09&type=Issues) | [@SaladRaider](https://github.com/search?q=repo%3Ajupyterhub%2Fnbgitpuller+involves%3ASaladRaider+updated%3A2020-08-01..2021-06-09&type=Issues) | [@samuelmanzer](https://github.com/search?q=repo%3Ajupyterhub%2Fnbgitpuller+involves%3Asamuelmanzer+updated%3A2020-08-01..2021-06-09&type=Issues) | [@sean-morris](https://github.com/search?q=repo%3Ajupyterhub%2Fnbgitpuller+involves%3Asean-morris+updated%3A2020-08-01..2021-06-09&type=Issues) | [@ttimbers](https://github.com/search?q=repo%3Ajupyterhub%2Fnbgitpuller+involves%3Attimbers+updated%3A2020-08-01..2021-06-09&type=Issues) | [@welcome](https://github.com/search?q=repo%3Ajupyterhub%2Fnbgitpuller+involves%3Awelcome+updated%3A2020-08-01..2021-06-09&type=Issues) | [@yuvipanda](https://github.com/search?q=repo%3Ajupyterhub%2Fnbgitpuller+involves%3Ayuvipanda+updated%3A2020-08-01..2021-06-09&type=Issues)
+
+## 0.9
+
+### 0.9.0 - 2020-09-1
+
+- Allow destination to be configured ([#42](https://github.com/jupyterhub/nbgitpuller/pull/42))
+- Made the checkout from the reset_deleted_files to use the origin. ([#111](https://github.com/jupyterhub/nbgitpuller/pull/111))
+- Update version. ([#112](https://github.com/jupyterhub/nbgitpuller/pull/112))
+- Update index.rst ([#113](https://github.com/jupyterhub/nbgitpuller/pull/113))
+- Use shallow clones by default ([#117](https://github.com/jupyterhub/nbgitpuller/pull/117))
+- updating theme ([#126](https://github.com/jupyterhub/nbgitpuller/pull/126))
+- Update ipynb with newer query parameters and toggles ([#127](https://github.com/jupyterhub/nbgitpuller/pull/127))
+- Add a mybinder.org tab to the link builder ([#129](https://github.com/jupyterhub/nbgitpuller/pull/129))
+- tab activation on link generator ([#132](https://github.com/jupyterhub/nbgitpuller/pull/132))
+- fixing bug ([#134](https://github.com/jupyterhub/nbgitpuller/pull/134))
+- Fix typo from ipynb link generator external tool reference ([#136](https://github.com/jupyterhub/nbgitpuller/pull/136))
+- Use the correct branch for contentRepo ([#138](https://github.com/jupyterhub/nbgitpuller/pull/138))
+- Fix file paths or application paths ([#140](https://github.com/jupyterhub/nbgitpuller/pull/140))
+- Make the environment repo branch required for binder ([#143](https://github.com/jupyterhub/nbgitpuller/pull/143))
+- Travis pypi deployment, README fixes ([#145](https://github.com/jupyterhub/nbgitpuller/pull/145))
+- Replace data-8 with jupyterhub ([#146](https://github.com/jupyterhub/nbgitpuller/pull/146))
+- CI: fix broken test assertions following --depth 1 by default ([#147](https://github.com/jupyterhub/nbgitpuller/pull/147))
+- CI: ensure tox run's flake8 as well ([#148](https://github.com/jupyterhub/nbgitpuller/pull/148))
+
+## 0.8
+
+### 0.8.0 2019-11-23
+
+- Link generator: init application type from query params ([#107](https://github.com/jupyterhub/nbgitpuller/pull/107))
+- Made the checkout from the reset_deleted_files to use the origin. ([#111](https://github.com/jupyterhub/nbgitpuller/pull/111))
+
+## 0.7
+
+### 0.7.2 - 2019-10-3
+
+- Bump version number ([#103](https://github.com/jupyterhub/nbgitpuller/pull/103))
+- Set authorship info on each commit, rather than repo-wide ([#104](https://github.com/jupyterhub/nbgitpuller/pull/104))
+- Bump version number ([#105](https://github.com/jupyterhub/nbgitpuller/pull/105))
+
+### 0.7.1 2019-10-3
+
+- Update version to 0.7.0. ([#100](https://github.com/jupyterhub/nbgitpuller/pull/100))
+- Fix legacy links with empty path ([#102](https://github.com/jupyterhub/nbgitpuller/pull/102))
+- Bump version number ([#103](https://github.com/jupyterhub/nbgitpuller/pull/103))
+
+### 0.7.0 2019-07-31
+
+- adding a link generator binder ([#49](https://github.com/jupyterhub/nbgitpuller/pull/49))
+- Clean up link_generator notebook / app ([#50](https://github.com/jupyterhub/nbgitpuller/pull/50))
+- add link to TLJH guide in readme ([#52](https://github.com/jupyterhub/nbgitpuller/pull/52))
+- updating link sanitizing ([#54](https://github.com/jupyterhub/nbgitpuller/pull/54))
+- adds link to a basic video instruction ([#56](https://github.com/jupyterhub/nbgitpuller/pull/56))
+- Add new link generator instructions ([#62](https://github.com/jupyterhub/nbgitpuller/pull/62))
+- adding new nbgitpuller link gen app ([#63](https://github.com/jupyterhub/nbgitpuller/pull/63))
+- Implement depth/shallow-clone support ([#67](https://github.com/jupyterhub/nbgitpuller/pull/67))
+- Made repo_dir an absolute path based on the server_root_dir. ([#71](https://github.com/jupyterhub/nbgitpuller/pull/71))
+- Serve gh pages from docs/ not gh-pages ([#73](https://github.com/jupyterhub/nbgitpuller/pull/73))
+- Pass nbapp along to GitPuller so it can read from our configuration ([#75](https://github.com/jupyterhub/nbgitpuller/pull/75))
+- Rework nbgitpuller link generator ([#76](https://github.com/jupyterhub/nbgitpuller/pull/76))
+- Generate URLs that can be launched from canvas ([#78](https://github.com/jupyterhub/nbgitpuller/pull/78))
+- Don't require including cloned dir name in path to open ([#79](https://github.com/jupyterhub/nbgitpuller/pull/79))
+- adding documentation ([#81](https://github.com/jupyterhub/nbgitpuller/pull/81))
+- circle config to push docs ([#82](https://github.com/jupyterhub/nbgitpuller/pull/82))
+- documentation clarification ([#88](https://github.com/jupyterhub/nbgitpuller/pull/88))
+- Redo documentation ([#92](https://github.com/jupyterhub/nbgitpuller/pull/92))
+- Allow git@example.com:repo links ([#97](https://github.com/jupyterhub/nbgitpuller/pull/97))
+
+## 0.6
+
+### 0.6.1 2018-07-19
+
+- Install Jupyter notebook extension by default, Add missing nbgitpuller.json file
+
+### 0.6.0 2018-07-18
+
+- Work with (and require) newer notebook version ([#46](https://github.com/jupyterhub/nbgitpuller/pull/46))
+- Update README.md ([#48](https://github.com/jupyterhub/nbgitpuller/pull/48))
diff --git a/nbgitpuller/LICENSE b/nbgitpuller/LICENSE
new file mode 100644
index 0000000000000000000000000000000000000000..c7c4ca7d4a41278d33bf19a3f0869cfb162630d0
--- /dev/null
+++ b/nbgitpuller/LICENSE
@@ -0,0 +1,29 @@
+BSD 3-Clause License
+
+Copyright (c) 2017, YuviPanda, Peter Veerman
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+* Redistributions of source code must retain the above copyright notice, this
+ list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+
+* Neither the name of the copyright holder nor the names of its
+ contributors may be used to endorse or promote products derived from
+ this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/nbgitpuller/MANIFEST.in b/nbgitpuller/MANIFEST.in
new file mode 100644
index 0000000000000000000000000000000000000000..607df237649251e00b8fe332f2d4648e7cea5378
--- /dev/null
+++ b/nbgitpuller/MANIFEST.in
@@ -0,0 +1,5 @@
+include *.md
+include LICENSE
+include setup.cfg
+recursive-include nbgitpuller/static *
+recursive-include nbgitpuller/templates *
diff --git a/nbgitpuller/README.md b/nbgitpuller/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..2fa08a4b30e577b0ba891c5fe61bf7a58d069c94
--- /dev/null
+++ b/nbgitpuller/README.md
@@ -0,0 +1,28 @@
+# [nbgitpuller](https://github.com/jupyterhub/nbgitpuller)
+
+
+[](https://github.com/jupyterhub/nbgitpuller/actions)
+[](https://circleci.com/gh/jupyterhub/nbgitpuller)
+[](https://pypi.python.org/pypi/nbgitpuller)
+[](https://github.com/jupyterhub/nbgitpuller/issues)
+[](https://discourse.jupyter.org/c/jupyterhub)
+[](https://gitter.im/jupyterhub/jupyterhub)
+
+`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)
+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
+information.
+
+## Installation
+
+```shell
+pip install nbgitpuller
+```
+
+## Example
+
+
diff --git a/nbgitpuller/RELEASE.md b/nbgitpuller/RELEASE.md
new file mode 100644
index 0000000000000000000000000000000000000000..981a3c56edd2ca46f66f91d8dfed69a2454d93b9
--- /dev/null
+++ b/nbgitpuller/RELEASE.md
@@ -0,0 +1,67 @@
+# 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.
+
+
+## Steps to make a release
+
+1. Checkout main and make sure it is up to date.
+
+ ```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
+ ```
+
+1. Set the `__version__` variable in
+ [`nbgitpuller/version.py`](nbgitpuller/version.py)
+ and make a commit.
+
+ ```
+ git add nbgitpuller/version.py
+ VERSION=... # e.g. 1.2.3
+ git commit -m "release $VERSION"
+ ```
+
+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.
+ ```
+ git add nbgitpuller/version.py
+ git commit -m "back to dev"
+ ```
+
+1. Push your two commits to main.
+
+ ```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
+ ```
+
+1. Create a git tag for the pushed release commit and push it.
+
+ ```shell
+ git tag -a $VERSION -m $VERSION HEAD~1
+
+ # then verify you tagged the right commit
+ git log
+
+ # then push it
+ git push $ORIGIN refs/tags/$VERSION
+ ```
+
+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.
diff --git a/nbgitpuller/_config.yml b/nbgitpuller/_config.yml
new file mode 100644
index 0000000000000000000000000000000000000000..c7418817439b2f071c93a4a6cee831e996123c0b
--- /dev/null
+++ b/nbgitpuller/_config.yml
@@ -0,0 +1 @@
+theme: jekyll-theme-slate
\ No newline at end of file
diff --git a/nbgitpuller/binder/link_generator.ipynb b/nbgitpuller/binder/link_generator.ipynb
new file mode 100644
index 0000000000000000000000000000000000000000..fea897ef7d93e6aa4ea5d3f13a4a42f5674e88a4
--- /dev/null
+++ b/nbgitpuller/binder/link_generator.ipynb
@@ -0,0 +1,164 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "# Generate `nbgitpuller` links for your JupyterHub\n",
+ "\n",
+ "When users click an `nbgitpuller` link pointing to your JupyterHub,\n",
+ "\n",
+ "1. They are asked to log in to the JupyterHub if they have not already\n",
+ "2. The git repository referred to in the nbgitpuller link is made up to date in their home directory (keeping local changes if there are merge conflicts)\n",
+ "3. They are shown the specific notebook / directory referred to in the nbgitpuller link.\n",
+ "\n",
+ "This is a great way to distribute materials to students.\n",
+ "\n",
+ "# Generate `nbgitpuller` links for your JupyterHub\n",
+ "\n",
+ "## Sequence of events when users click an `nbgitpuller` link pointing to your JupyterHub,\n",
+ "\n",
+ "1. They are asked to log in to the JupyterHub if they have not already\n",
+ "2. The git repository referred to in the nbgitpuller link is made up to date in their home directory (keeping local changes if there are merge conflicts)\n",
+ "3. They are shown the specific notebook / directory referred to in the nbgitpuller link.\n",
+ "\n",
+ "This is a great way to distribute materials to students.\n",
+ "\n",
+ "## Canvas LMS: Assignment Links vs Custom Fields\n",
+ "\n",
+ "The Canvas LMS expects the assignment link to include URL encoded parameters since the request is sent to the External Tool as a POST request (in this case JupyterHub is the External Tool). However, all characters (even those considered safe) after the domain and `next=` part should be URL encoded, such as the `/`, `&`, and `=` characters.\n",
+ "\n",
+ "The `Custom Fields` text box in the App -> Settings section, on the other hand, does not expect all characters to be URL encoded. The `/` characters that are assigned as part of the query parameter values should be encoded, but not the `&` and `=` characters.\n",
+ "\n",
+ "## Usage\n",
+ "\n",
+ "- **Assignment Link**: creates a string value which represents an `Assignment` link by toggling the check box next to the `is_assignment_link` label. If unchecked, the tool will create a string to add to the Custom Field section.\n",
+ "- **Jupyter Lab Link**: creates a string value which redirects the user to a `Jupyter Lab` workspace instead of the `Jupyter Classic` workspace.\n",
+ "- **LTI Launches**: adds the route associated to the LTI 1.1 login handler. If disabled, it is assumed that the user is using the default authentication class bound to the root of the `domain_url` value.\n",
+ "- **Default Values**: to avoid having to enter the same values in the widget's text fields on a repetitive basis, add the string values to the function's parameters. For example, the `branch` parameter defaults to `master`."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 6,
+ "metadata": {},
+ "outputs": [
+ {
+ "output_type": "display_data",
+ "data": {
+ "text/plain": "'https://my.hub.com/hub/lti/launch?next=%2Fuser-redirect%2Fgit-pull?repo%3D%26branch%3Dmaster%26urlpath%3Dlab%252Ftree%252F.%252F%253Fautodecode'"
+ },
+ "metadata": {}
+ }
+ ],
+ "source": [
+ "import os\n",
+ "from ipywidgets import interact\n",
+ "from urllib.parse import urlunparse, urlparse, urlencode, parse_qs, parse_qsl, quote\n",
+ "from IPython.display import Markdown\n",
+ "\n",
+ "\n",
+ "@interact\n",
+ "def make_launch_link(is_assignment_link=True, is_jupyterlab=True, is_lti11=True, branch='master', hub_url='https://my.hub.com', repo_url='', urlpath=''):\n",
+ " \"\"\"\n",
+ " Generate a launch request which clones and merges source files from a git-based\n",
+ " repository.\n",
+ "\n",
+ " Args:\n",
+ " is_assignment_link (bool): set to True to create a full assignment link, defaults to True.\n",
+ " is_jupyterlab (bool): set to True to launch Jupyter Lab workspaces, defaults to True.\n",
+ " is_lti11 (bool): set to True to initiate launch requests with the LTI 1.1 standard.\n",
+ " branch (str): git repo branch\n",
+ " hub_url (str): full hub url which needs to include scheme (http or https) and netloc (full domain).\n",
+ " repo_url (str): full git repo url which needs to include scheme (http or https), netloc (full domain) and path.\n",
+ " url_path (str): a path to redirect users to after the workspace has successfully spawned (started).\n",
+ "\n",
+ " Returns:\n",
+ " An interactive IPython.display.Markdown object.\n",
+ " \"\"\"\n",
+ "\n",
+ " # Parse the query to its constituent parts\n",
+ " domain_scheme, domain_netloc, domain_path, domain_params, domain_query_str, domain_fragment = urlparse(hub_url.strip())\n",
+ " \n",
+ " repo_scheme, repo_netloc, repo_path, repo_params, repo_query_str, repo_fragment = urlparse(repo_url.strip())\n",
+ " folder_from_repo_url_path = os.path.basename(os.path.normpath(repo_path))\n",
+ " \n",
+ " # Make sure the path doesn't contain multiple slashes\n",
+ " if not domain_path.endswith('/'):\n",
+ " domain_path += '/'\n",
+ " domain_path += 'user-redirect/git-pull'\n",
+ " \n",
+ " # With Canvas using LTI 11 Assignment launch requests all characters after the netloc are considered unsafe.\n",
+ " # When adding custom parameters within the App Settings -> Custom Fields section, only items after the \n",
+ " path_encoded = ''\n",
+ " if is_assignment_link:\n",
+ " path_encoded = quote(domain_path, safe='')\n",
+ " else:\n",
+ " path_encoded = quote(domain_path)\n",
+ "\n",
+ " path_redirect_url = f'next={path_encoded}'\n",
+ " if is_lti11:\n",
+ " assignment_link_path = f'/hub/lti/launch?next={path_encoded}'\n",
+ " else:\n",
+ " assignment_link_path = f'/hub?next={path_encoded}'\n",
+ " \n",
+ " # Create a tuple of query params from original domain link\n",
+ " query_params_from_hub_url = parse_qsl(domain_query_str, keep_blank_values=True)\n",
+ " \n",
+ " # Set path based on whether or not the user would like to spawn JupyterLab or Jupyter Classic\n",
+ " urlpath_workspace = ''\n",
+ " if is_jupyterlab:\n",
+ " urlpath_workspace = f'lab/tree/{folder_from_repo_url_path}/{urlpath}?autodecode'\n",
+ " else:\n",
+ " urlpath_workspace = f'tree/{folder_from_repo_url_path}/{urlpath}'\n",
+ " \n",
+ " # Create a tuple of query params for git functionality. Check whether or not we want to launch with\n",
+ " # jupyterlab to add additional items to the path.\n",
+ " query_params_for_git = [('repo', repo_url), ('branch', branch), ('urlpath', urlpath_workspace)]\n",
+ " \n",
+ " # Merge query params into one list of tuples\n",
+ " query_params_all = query_params_from_hub_url + query_params_for_git\n",
+ " \n",
+ " # First build urlencoded query params where the &, =, and / are considered safe. Then, percent encode\n",
+ " # all characters.\n",
+ " encoded_query_params = urlencode(query_params_all)\n",
+ " encoded_query_params_without_safe_chars = quote(urlencode(query_params_all), safe='')\n",
+ " \n",
+ " assignment_link_url = urlunparse((domain_scheme, domain_netloc, assignment_link_path, domain_params, encoded_query_params_without_safe_chars, domain_fragment))\n",
+ " path_url = urlunparse(('', '', path_redirect_url, domain_params, encoded_query_params, domain_fragment))\n",
+ " \n",
+ " if is_assignment_link:\n",
+ " return assignment_link_url\n",
+ " return path_url"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": []
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "display_name": "Python 3",
+ "language": "python",
+ "name": "python3"
+ },
+ "language_info": {
+ "codemirror_mode": {
+ "name": "ipython",
+ "version": 3
+ },
+ "file_extension": ".py",
+ "mimetype": "text/x-python",
+ "name": "python",
+ "nbconvert_exporter": "python",
+ "pygments_lexer": "ipython3",
+ "version": "3.8.1-final"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 2
+}
diff --git a/nbgitpuller/binder/postBuild b/nbgitpuller/binder/postBuild
new file mode 100644
index 0000000000000000000000000000000000000000..829840871c5be8e3373dfbda6d6597b9b9074417
--- /dev/null
+++ b/nbgitpuller/binder/postBuild
@@ -0,0 +1,3 @@
+#!/bin/bash
+jupyter nbextension enable --py --sys-prefix appmode
+jupyter serverextension enable --py --sys-prefix appmode
\ No newline at end of file
diff --git a/nbgitpuller/binder/requirements.txt b/nbgitpuller/binder/requirements.txt
new file mode 100644
index 0000000000000000000000000000000000000000..87cc563892a6b234540d41b4f1a0c099e7d82077
--- /dev/null
+++ b/nbgitpuller/binder/requirements.txt
@@ -0,0 +1,2 @@
+ipywidgets
+appmode
\ No newline at end of file
diff --git a/nbgitpuller/dev-requirements.txt b/nbgitpuller/dev-requirements.txt
new file mode 100644
index 0000000000000000000000000000000000000000..37b15b922cc26cee573f13cf62f31bdd8f7351e4
--- /dev/null
+++ b/nbgitpuller/dev-requirements.txt
@@ -0,0 +1,6 @@
+six
+pytest
+pytest-cov
+flake8
+nbclassic
+nest-asyncio
diff --git a/nbgitpuller/docs/Makefile b/nbgitpuller/docs/Makefile
new file mode 100644
index 0000000000000000000000000000000000000000..9764015b498c764e5b11b7b40b4b401925ed2e53
--- /dev/null
+++ b/nbgitpuller/docs/Makefile
@@ -0,0 +1,20 @@
+# Minimal makefile for Sphinx documentation
+#
+
+# You can set these variables from the command line.
+SPHINXOPTS =
+SPHINXBUILD = sphinx-build
+SPHINXPROJ = Binder
+SOURCEDIR = .
+BUILDDIR = _build
+
+# Put it first so that "make" without argument is like "make help".
+help:
+ @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
+
+.PHONY: help Makefile
+
+# Catch-all target: route all unknown targets to Sphinx using the new
+# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
+%: Makefile
+ @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
diff --git a/nbgitpuller/docs/_static/link_gen/link.js b/nbgitpuller/docs/_static/link_gen/link.js
new file mode 100644
index 0000000000000000000000000000000000000000..dd371c81c9ec1afcf07a9940caa98c059d7adaa5
--- /dev/null
+++ b/nbgitpuller/docs/_static/link_gen/link.js
@@ -0,0 +1,276 @@
+// Pure function that generates an nbgitpuller URL
+function generateRegularUrl(hubUrl, urlPath, repoUrl, branch) {
+
+ // assume hubUrl is a valid URL
+ var url = new URL(hubUrl);
+
+ url.searchParams.set('repo', repoUrl);
+
+ if (urlPath) {
+ url.searchParams.set('urlpath', urlPath);
+ }
+
+ if (branch) {
+ url.searchParams.set('branch', branch);
+ }
+
+ if (!url.pathname.endsWith('/')) {
+ url.pathname += '/'
+ }
+ url.pathname += 'hub/user-redirect/git-pull';
+
+ return url.toString();
+}
+
+function generateCanvasUrl(hubUrl, urlPath, repoUrl, branch) {
+ // assume hubUrl is a valid URL
+ var url = new URL(hubUrl);
+
+ var nextUrlParams = new URLSearchParams();
+
+ nextUrlParams.append('repo', repoUrl);
+
+ if (urlPath) {
+ nextUrlParams.append('urlpath', urlPath);
+ }
+
+ if (branch) {
+ nextUrlParams.append('branch', branch);
+ }
+
+ var nextUrl = '/hub/user-redirect/git-pull?' + nextUrlParams.toString();
+
+ if (!url.pathname.endsWith('/')) {
+ url.pathname += '/'
+ }
+ url.pathname += 'hub/lti/launch'
+ url.searchParams.append('next', nextUrl);
+
+ return url.toString();
+}
+
+function generateBinderUrl(hubUrl, userName, repoName, branch, urlPath,
+ contentRepoUrl, contentRepoBranch) {
+
+ var url = new URL(hubUrl);
+
+ var nextUrlParams = new URLSearchParams();
+
+ nextUrlParams.append('repo', contentRepoUrl);
+
+ if (urlPath) {
+ nextUrlParams.append('urlpath', urlPath);
+ }
+
+ if (contentRepoBranch) {
+ nextUrlParams.append('branch', contentRepoBranch);
+ }
+
+ var nextUrl = 'git-pull?' + nextUrlParams.toString();
+
+ var path = '/v2/gh/';
+ url.pathname = path.concat(userName, "/", repoName, "/", branch);
+ url.searchParams.append('urlpath', nextUrl);
+
+ return url.toString();
+}
+
+var apps = {
+ classic: {
+ title: 'Classic Notebook',
+ generateUrlPath: function (path) { return 'tree/' + path; },
+ },
+ jupyterlab: {
+ title: 'JupyterLab',
+ generateUrlPath: function (path) { return 'lab/tree/' + path; }
+ },
+ shiny: {
+ title: 'Shiny',
+ generateUrlPath: function (path) {
+ // jupyter-shiny-proxy requires everything to end with a trailing slash
+ if (!path.endsWith("/")) {
+ path = path + "/";
+ }
+ return 'shiny/' + path;
+ }
+ },
+ rstudio: {
+ title: 'RStudio',
+ generateUrlPath: function (path) { return 'rstudio/'; }
+ }
+}
+
+function changeTab(div) {
+ var hub = document.getElementById("hub");
+ var hub_help_text = document.getElementById("hub-help-text");
+ var env_repo = document.getElementById("repo");
+ var env_repo_branch = document.getElementById("branch");
+ var env_repo_help_text = document.getElementById("env-repo-help-text");
+ var content_repo = document.getElementById("content-repo-group");
+ var content_branch = document.getElementById("content-branch-group");
+ var id = div.id;
+
+ if (id.includes("binder")) {
+ hub.placeholder = "https://mybinder.org";
+ hub.value = "https://mybinder.org";
+ hub_help_text.hidden = true;
+ hub.labels[0].innerHTML = "BinderHub URL";
+ env_repo.labels[0].innerHTML = "Git Environment Repository URL";
+ env_repo_help_text.hidden = false;
+ env_repo_branch.required = true;
+ env_repo_branch.pattern = ".+";
+ content_repo.hidden = false;
+ content_branch.hidden = false;
+ } else {
+ hub.placeholder = "https://hub.example.com";
+ hub_help_text.hidden = false;
+ hub.labels[0].innerHTML = "JupyterHub URL";
+ env_repo.labels[0].innerHTML = "Git Repository URL";
+ env_repo_help_text.hidden = true;
+ env_repo_branch.required = false;
+ content_repo.hidden = true;
+ content_branch.hidden = true;
+ }
+}
+
+/**
+ * Return name of directory git will clone given repo to.
+ *
+ * nbgitpuller needs to redirect users to *inside* the directory it
+ * just cloned. We copy the logic git itself uses to determine that.
+ * See https://github.com/git/git/blob/1c52ecf4ba0f4f7af72775695fee653f50737c71/builtin/clone.c#L276
+ */
+function generateCloneDirectoryName(gitCloneUrl) {
+ var lastPart = gitCloneUrl.split('/').slice(-1)[0];
+ return lastPart.split(':').slice(-1)[0].replace(/(\.git|\.bundle)?/, '');
+}
+
+function displayLink() {
+ var form = document.getElementById('linkgenerator');
+
+ form.classList.add('was-validated');
+ if (form.checkValidity()) {
+ var hubUrl = document.getElementById('hub').value;
+ var repoUrl = document.getElementById('repo').value;
+ var branch = document.getElementById('branch').value;
+ var contentRepoUrl = document.getElementById('content-repo').value;
+ var contentRepoBranch = document.getElementById('content-branch').value;
+ var filePath = document.getElementById('filepath').value;
+ var appName = form.querySelector('input[name="app"]:checked').value;
+ var activeTab = document.querySelector(".nav-link.active").id;
+
+ if (appName === 'custom') {
+ var urlPath = document.getElementById('urlpath').value;
+ } else {
+ var repoName = generateCloneDirectoryName(repoUrl);
+ var urlPath;
+ if (activeTab === "tab-auth-binder") {
+ var contentRepoName = new URL(contentRepoUrl).pathname.split('/').pop().replace(/\.git$/, '');
+ urlPath = apps[appName].generateUrlPath(contentRepoName + '/' + filePath);
+ } else {
+ urlPath = apps[appName].generateUrlPath(repoName + '/' + filePath);
+ }
+ }
+
+ if (activeTab === "tab-auth-default") {
+ document.getElementById('default-link').value = generateRegularUrl(
+ hubUrl, 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
+ // 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];
+ document.getElementById('binder-link').value = generateBinderUrl(
+ hubUrl, userName, repoName, branch, urlPath, contentRepoUrl, contentRepoBranch
+ );
+ }
+ }
+}
+function populateFromQueryString() {
+ // preseed values if specified in the url
+ var params = new URLSearchParams(window.location.search);
+ // Parameters are read from query string, and fields are set to them
+ var allowedParams = ['hub', 'repo', 'content-repo', 'branch', 'app', 'urlpath'];
+ if (params.has("urlpath")) {
+ // setting urlpath implies a custom app
+ document.getElementById('app-custom').checked = true;
+ }
+ for (var i = 0; i < allowedParams.length; i++) {
+ var param = allowedParams[i];
+ if (params.has(param)) {
+ if ((param === 'app') && !params.has("urlpath")) {
+ radioId = 'app-' + params.get(param).toLowerCase();
+ document.getElementById(radioId).checked = true;
+ } else {
+ document.getElementById(param).value = params.get(param);
+ }
+ }
+ }
+}
+
+/**
+ * Main loop of the program.
+ *
+ * Called whenever any state changes (input received, page loaded, etc).
+ * Should turn on / off elements based only on current state, and display the link
+ *
+ * Sort of react-ish.
+ */
+function render() {
+ var form = document.getElementById('linkgenerator');
+ var appName = form.querySelector('input[name="app"]:checked').value;
+
+ if (appName == 'custom') {
+ document.getElementById('urlpath').disabled = false;
+ document.getElementById('filepath').disabled = true;
+ } else {
+ document.getElementById('urlpath').disabled = true;
+
+ var app = apps[appName];
+ if (!app.generateUrlPath) {
+ document.getElementById('filepath').disabled = true;
+ } else {
+ document.getElementById('filepath').disabled = false;
+ }
+ }
+ displayLink();
+}
+
+/**
+ * Entry point
+ */
+function main() {
+ // Hook up any changes in form elements to call render()
+ document.querySelectorAll('#linkgenerator input[type="radio"]').forEach(
+ function (element) {
+ element.addEventListener('change', render);
+ }
+ )
+ document.querySelectorAll('#linkgenerator input[type="text"], #linkgenerator input[type="url"]').forEach(
+ function (element) {
+ element.addEventListener('input', render);
+ }
+ )
+
+ populateFromQueryString();
+
+ // 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()
+ }
+ }
+
+ // Do an initial render, to make sure our disabled / enabled properties are correctly set
+ render();
+}
+
+window.onload = main;
diff --git a/nbgitpuller/docs/_static/nbpuller.gif b/nbgitpuller/docs/_static/nbpuller.gif
new file mode 100644
index 0000000000000000000000000000000000000000..9ccbe76d4ee260dbbcdca0007f382a49e2166735
Binary files /dev/null and b/nbgitpuller/docs/_static/nbpuller.gif differ
diff --git a/nbgitpuller/docs/_templates/layout.html b/nbgitpuller/docs/_templates/layout.html
new file mode 100644
index 0000000000000000000000000000000000000000..5598856ab5c56e59071dac002bf1bbdfdbb29fff
--- /dev/null
+++ b/nbgitpuller/docs/_templates/layout.html
@@ -0,0 +1,14 @@
+{%- extends "sphinx_book_theme/layout.html" %}
+
+
+{% block extrahead %}
+{% if pagename == 'link' %}
+
+{% endif %}
+{{ super() }}
+{% endblock %}
diff --git a/nbgitpuller/docs/conf.py b/nbgitpuller/docs/conf.py
new file mode 100644
index 0000000000000000000000000000000000000000..66fb9cb2580fae75fd2859d420db2a998a90c30e
--- /dev/null
+++ b/nbgitpuller/docs/conf.py
@@ -0,0 +1,164 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+
+github_doc_root = "https://github.com/rtfd/recommonmark/tree/master/doc/"
+
+
+def setup(app):
+ app.add_stylesheet("custom.css")
+ app.add_javascript("link_gen/link.js")
+
+
+# -- General configuration ------------------------------------------------
+
+# If your documentation needs a minimal Sphinx version, state it here.
+#
+# needs_sphinx = '1.0'
+
+# Add any Sphinx extension module names here, as strings. They can be
+# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
+# ones.
+extensions = [
+ "myst_parser",
+ "sphinx.ext.intersphinx",
+]
+
+# Add any paths that contain templates here, relative to this directory.
+templates_path = ["_templates"]
+
+# The suffix(es) of source filenames.
+# You can specify multiple suffix as a list of string:
+
+source_suffix = [".rst", ".md"]
+
+
+# The root toctree document.
+root_doc = master_doc = "index"
+
+# General information about the project.
+project = "nbgitpuller"
+copyright = "2017, The nbgitpuller Team"
+author = "The nbgitpuller Team"
+
+# The version info for the project you're documenting, acts as replacement for
+# |version| and |release|, also used in various other places throughout the
+# built documents.
+#
+# The short X.Y version.
+version = "0.1b"
+# The full version, including alpha/beta/rc tags.
+release = "0.1b"
+
+# The language for content autogenerated by Sphinx. Refer to documentation
+# for a list of supported languages.
+#
+# 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
+
+# List of patterns, relative to source directory, that match files and
+# directories to ignore when looking for source files.
+# This patterns also effect to html_static_path and html_extra_path
+exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"]
+
+html_sidebars = {"**": ["globaltoc.html", "relations.html", "searchbox.html"]}
+
+# The name of the Pygments (syntax highlighting) style to use.
+pygments_style = "sphinx"
+
+# If true, `todo` and `todoList` produce output, else they produce nothing.
+todo_include_todos = False
+
+# -- Options for HTML output ----------------------------------------------
+
+# The theme to use for HTML and HTML Help pages. See the documentation for
+# a list of builtin themes.
+#
+html_theme = "sphinx_book_theme"
+
+# Theme options are theme-specific and customize the look and feel of a theme
+# further. For a list of options available for each theme, see the
+# documentation.
+#
+# html_theme_options = {}
+
+html_context = {
+ "github_user": "jupyterhub",
+ "github_repo": "nbgitpuller",
+ "github_version": "main",
+ "doc_path": "doc",
+ "source_suffix": source_suffix,
+}
+
+html_theme_options = {
+ "repository_url": "https://github.com/jupyterhub/nbgitpuller",
+ "use_issues_button": True,
+ "use_repository_button": True
+}
+
+# Add any paths that contain custom static files (such as style sheets) here,
+# relative to this directory. They are copied after the builtin static files,
+# so a file named "default.css" will overwrite the builtin "default.css".
+html_static_path = ["_static"]
+
+
+# -- Options for HTMLHelp output ------------------------------------------
+
+# Output file base name for HTML help builder.
+htmlhelp_basename = "nbgitpullerdoc"
+
+
+# -- Options for LaTeX output ---------------------------------------------
+
+latex_elements = {
+ # The paper size ('letterpaper' or 'a4paper').
+ #
+ # 'papersize': 'letterpaper',
+ # The font size ('10pt', '11pt' or '12pt').
+ #
+ # 'pointsize': '10pt',
+ # Additional stuff for the LaTeX preamble.
+ #
+ # 'preamble': '',
+ # Latex figure (float) alignment
+ #
+ # 'figure_align': 'htbp',
+}
+
+# Grouping the document tree into LaTeX files. List of tuples
+# (source start file, target name, title,
+# author, documentclass [howto, manual, or own class]).
+latex_documents = [
+ (
+ root_doc,
+ "nbgitpuller.tex",
+ "nbgitpuller Documentation",
+ "The nbgitpuller Team",
+ "manual",
+ )
+]
+
+
+# -- Options for manual page output ---------------------------------------
+
+# One entry per manual page. List of tuples
+# (source start file, name, description, authors, manual section).
+man_pages = [(root_doc, "nbgitpuller", "nbgitpuller Documentation", [author], 1)]
+
+
+# -- Options for Texinfo output -------------------------------------------
+
+# Grouping the document tree into Texinfo files. List of tuples
+# (source start file, target name, title, author,
+# dir menu entry, description, category)
+texinfo_documents = [
+ (
+ root_doc,
+ "nbgitpuller",
+ "nbgitpuller Documentation",
+ author,
+ "nbgitpuller",
+ "One line description of project.",
+ "Miscellaneous",
+ )
+]
diff --git a/nbgitpuller/docs/contributing.md b/nbgitpuller/docs/contributing.md
new file mode 100644
index 0000000000000000000000000000000000000000..0df9b8f821f8cb2827fcb9cb0a4908f786a9f14c
--- /dev/null
+++ b/nbgitpuller/docs/contributing.md
@@ -0,0 +1,133 @@
+# Contributing
+
+## Setup
+
+nbgitpuller is a jupyter extension that works with both the
+[classic Notebook Server](https://jupyter-notebook.readthedocs.io/en/stable/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.
+
+1. Fork the nbgitpuller repository and `git clone` it to your local computer.
+
+2. Inside the nbgitpuller clone on your local machine, setup a virtual
+ environment to do development in
+
+ ```bash
+ python3 -m venv venv
+ source venv/bin/activate
+ ```
+
+3. Install development time dependencies in this virtual environment
+
+ ```bash
+ pip install -r dev-requirements.txt
+ ```
+
+4. Install nbgitpuller with its dependencies in this virtual environment
+
+ ```bash
+ pip install -e .
+ ```
+
+5. Install the NodeJS dependencies from package.json.
+
+ ```bash
+ npm install
+ ```
+
+6. Create the JS and CSS bundles.
+
+ ```bash
+ npm run webpack
+ ```
+
+7. Enable the nbgitpuller extension:
+ * as a jupyter serverextension (classic Notebook Server extension)
+
+ ```bash
+ jupyter serverextension enable --sys-prefix nbgitpuller
+ ```
+ * as a jupyter server extension
+ ```bash
+ jupyter server extension enable --sys-prefix nbgitpuller
+ ```
+
+8. Start the notebook server:
+
+ * You can either start the classical Notebook server.
+ This will open the classic notebook in your web
+ browser, and automatically authenticate you as a side effect.
+
+ ```bash
+ jupyter notebook
+ ```
+
+ * Or you can start the new Jupyter Server.
+ ```bash
+ jupyter server
+ ```
+ This won't open any notebook interface, unless you don't enable one
+ ([`nbclassic`](https://github.com/jupyterlab/nbclassic) or [`jupyterlab`](https://github.com/jupyterlab/jupyterlab))
+ as a jupyter server extension.
+ ```bash
+ jupyter server extension enable --sys-prefix nbclassic
+ ```
+ or
+ ```bash
+ jupyter server extension enable --sys-prefix jupyterlab
+ ```
+
+9. You can now test nbgitpuller locally, by hitting the `/git-pull` url with any
+ of the [URL query parameters](topic/url-options.rst). For example, to pull the
+ [data-8/textbook](https://github.com/data-8/textbook) repository's `gh-pages`
+ branch, you can use the following URL:
+
+ ```
+ http://localhost:8888/git-pull?repo=https://github.com/data-8/textbook&branch=gh-pages
+ ```
+
+10. If you make changes to nbgitpuller's python code, you need to restart the `jupyter notebook`
+ process (started in step 5) to see your changes take effect. This is not needed if
+ you are only working on the javascript or css.
+
+## Running the flake8 linter
+
+[flake8](https://flake8.pycqa.org/en/latest/) is used to validate python coding style. The
+flake8 config is in `.flake8`, and is not super strict. You should be able to run
+`flake8` in the root directory of the repository to get a list of issues to be fixed.
+
+## Running tests
+
+[pytest](https://docs.pytest.org/) is used to run unit and integration tests,
+under the `tests/` directory. If you add new functionality, you should also add
+tests to cover it. You can run the tests locally with `py.test tests/`
+
+## Building documentation
+
+[sphinx](https://www.sphinx-doc.org/) is used to write and maintain documentation, under
+the `docs/` directory. If you add any new functionality, you should write documentaiton
+for it as well. A mix of [reStructuredText](https://www.sphinx-doc.org/en/master/usage/restructuredtext/basics.html)
+and [MyST Markdown](https://myst-parser.readthedocs.io) is used to write our documentation,
+although we would like to migrate purely to MyST markdown in the future.
+
+1. Install the packages needed to build the documentation
+
+ ```bash
+ pip install -r docs/doc-requirements.txt
+ ```
+
+2. Build the documentation by using `make` inside the `docs` folder. This will
+ internally call `sphinx`
+
+ ```bash
+ cd docs
+ make html
+ ```
+
+3. Preview the documentation by opening `_build/html/index.html` file in
+ your browser. From inside the `docs` folder, you can run either
+ `open _build/html/index.html` (on MacOS) or `xdg-open _build/html/index.html`
+ to quickly open the file in the browser.
+
+4. You can run `make html` again after making further changes to see their
+ effects.
diff --git a/nbgitpuller/docs/doc-requirements.txt b/nbgitpuller/docs/doc-requirements.txt
new file mode 100644
index 0000000000000000000000000000000000000000..d8c4759b68b34db3c6d4c71a333c0075e11ad56d
--- /dev/null
+++ b/nbgitpuller/docs/doc-requirements.txt
@@ -0,0 +1,3 @@
+myst_parser
+sphinx_copybutton
+sphinx-book-theme
diff --git a/nbgitpuller/docs/index.rst b/nbgitpuller/docs/index.rst
new file mode 100644
index 0000000000000000000000000000000000000000..ef65d01fdb246dc5d9bfbd34594aa30c185eba76
--- /dev/null
+++ b/nbgitpuller/docs/index.rst
@@ -0,0 +1,96 @@
+===========
+nbgitpuller
+===========
+
+``nbgitpuller`` lets you distribute content in a git repository to your
+students by having them click a simple link. :ref:`Automatic, opinioned
+conflict resolution ` ensures that your students are
+never exposed to ``git`` directly. It is primarily used with a JupyterHub,
+but can also work on students' local laptops.
+
+.. image:: _static/nbpuller.gif
+
+When to use nbgitpuller?
+========================
+
+You should use nbgitpuller when:
+
+#. You are running a JupyterHub for a class & want an easy way to distribute
+ materials to your students without them having to understand what git is.
+#. You have a different out of band method for collecting completed
+ assignments / notebooks from students, since they can not just 'push it
+ back' via git.
+
+You should **not** use nbgitpuller when:
+
+#. You are an instructor using a JupyterHub / running notebooks locally to
+ create materials and push them to a git repository. You should just use
+ git directly, since the assumptions and design of nbgitpuller **will**
+ surprise you in unexpected ways if you are pushing with git but pulling
+ with nbgitpuller.
+#. Your students are performing manual git operations on the git repository
+ cloned as well as using nbgitpuller. Mixing manual git operations +
+ automatic nbgitpuller operations is going to cause surprises on an ongoing
+ basis, and should be avoided.
+
+Installation
+============
+
+If you already have a JupyterHub, you can follow :ref:`these installation
+instructions ` to install nbgitpuller there. They should also
+work for installation on a local Jupyter Notebook installation without
+JupyterHub.
+
+If you do *not* have a JupyterHub, we recommend trying out `The Littlest
+JupyterHub `_ to set one up. It comes built
+in with nbgitpuller.
+
+Using nbgitpuller as an instructor
+==================================
+
+Once installed, you create a specially crafted web link (called
+*nbgitpuller links*) and send to your students via any method you like -
+course website, LMS, email, etc. This link will contain at least the
+following information:
+
+#. The location of the JupyterHub you are sending them to.
+#. The git repository where you have published your content.
+#. Optionally, a particular file or directory you want to automatically
+ open for your students once the repository has been synchronized. Note the entire repository will be copied, not just the specified file.
+
+The first time a particular student clicks the link, a local copy of the
+repository is made for the student. On successive clicks, the latest version
+of the remote repository is fetched, and merged automatically with the
+student's local copy using a :ref:`series of rules `
+that ensure students never get merge conflicts.
+
+You can generate such *nbgitpuller links* with the `generator
+`_.
+
+There is also a video showing you how to use nbgitpuller
+
+.. raw:: html
+
+
+
+If you are interested in the details of available options when creating
+the link, we have a :ref:`list of options ` as well.
+
+Full Contents
+=============
+
+.. toctree::
+ :maxdepth: 2
+
+ install
+ contributing
+ topic/automatic-merging
+ topic/url-options
+ topic/repo-best-practices
+ link
diff --git a/nbgitpuller/docs/install.rst b/nbgitpuller/docs/install.rst
new file mode 100644
index 0000000000000000000000000000000000000000..98f42f080411b6357409a50ffe288b8cb0414817
--- /dev/null
+++ b/nbgitpuller/docs/install.rst
@@ -0,0 +1,26 @@
+.. _install:
+
+============
+Installation
+============
+
+You can install ``nbgitpuller`` from PyPI with ``pip`` in the same
+environment where your jupyter notebook package is installed.
+
+.. code:: bash
+
+ pip install nbgitpuller
+
+Troubleshooting
+===============
+
+nbgitpuller link shows `404 Not Found`
+--------------------------------------
+
+If you are on an old version of Jupyter Notebook, you might get a `404 Not Found`
+error when trying to access an nbgitpuller link. You might need to manually enable
+the server extension that handles nbgitpuller.
+
+.. code:: bash
+
+ jupyter serverextension enable nbgitpuller --sys-prefix
diff --git a/nbgitpuller/docs/link.rst b/nbgitpuller/docs/link.rst
new file mode 100644
index 0000000000000000000000000000000000000000..a39395d3420c0666922fa3c5dab646dfb9844e6c
--- /dev/null
+++ b/nbgitpuller/docs/link.rst
@@ -0,0 +1,188 @@
+nbgitpuller link generator
+==========================
+
+Use the following form to create your own ``nbgitpuller`` links.
+
+.. raw:: html
+
+
+
+
+
+
+
+**Pre-populating some fields in the link generator**
+
+You can pre-populate some fields in order to make it easier for some
+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
+* ``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
+
+
+**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:
+
+* ``?tab=binder`` - activates the Binder tab
+* ``?tab=canvas`` - activates the Canvas tab.
diff --git a/nbgitpuller/docs/topic/automatic-merging.rst b/nbgitpuller/docs/topic/automatic-merging.rst
new file mode 100644
index 0000000000000000000000000000000000000000..5aa6e4ec587d2795abe03b35e3c361977f3601f4
--- /dev/null
+++ b/nbgitpuller/docs/topic/automatic-merging.rst
@@ -0,0 +1,61 @@
+.. _topic/automatic-merging:
+
+==========================
+Automatic Merging Behavior
+==========================
+
+``nbgitpuller`` tries to make sure the end user who clicked the link
+**never** has to manually interact with the git repo. This requires us to
+make some opinionated choices on how we handle various cases where both the
+student (end user) and instructor (author of the repo) repo have modified the
+repository.
+
+Here, we describe how we handle the various possible cases each time the
+student clicks the nbgitpuller link.
+
+Case 1: The instructor changed a file that the student has not changed
+======================================================================
+
+The student's changes are left alone, and the instructor's changes are pulled
+in to the local copy. Most common case. This is also what happens when the
+instructor adds a new file / directory.
+
+Case 2: Student & instructor changed different lines in same file
+=================================================================
+
+Very similar to case 1 - the student's changes are left alone, and the
+instructor's changes are merged in to the existing local file.
+
+Case 3: Student & instructor change same lines in same file
+===========================================================
+
+In this case, we **always keep the student's changes**. We want to never
+accidentally lose a student's changes - ``nbgitpuller`` will not eat your
+homework.
+
+Case 4: Student deletes file locally, but instructor doesn't
+============================================================
+
+If the student has deleted a file locally, but the file is still present in
+the remote repo, the file from the remote repo is pulled into the student's
+directory. This enables the use case where a student wants to 'start over'
+a file after having made many changes to it. They can simply delete the file,
+click the nbgitpuller link again, and get a fresh copy.
+
+Case 5: Student creates file manually, but instructor adds file with same name
+==============================================================================
+
+As an example, let's say the student manually creates a file named
+``Untitled141.ipynb`` in the directory where nbgitpuller has pulled a
+repository. At some point afterwards, the instructor creates a file *also*
+named ``Untitled141.ipynb`` and pushes it to the repo.
+
+When the student clicks the nbgitpuller link next, we want to make sure we
+don't destroy the student's work. Since they were created in two different
+places, the likelihood of them being mergeable is low. So we **rename** the
+student's file, and pull the instructor's file. So the student's
+``Untitled141.ipynb`` file will be renamed to
+``Untitled141_.ipynb``, and the instructor's file will be kept at
+``Untitled141.ipynb``.
+
+This is a fairly rare case in our experience.
diff --git a/nbgitpuller/docs/topic/repo-best-practices.md b/nbgitpuller/docs/topic/repo-best-practices.md
new file mode 100644
index 0000000000000000000000000000000000000000..025b6dfb88619ebd256034a03823fe8dabc45e34
--- /dev/null
+++ b/nbgitpuller/docs/topic/repo-best-practices.md
@@ -0,0 +1,41 @@
+# Content git repository best practices
+
+Sometimes, git's flexibility can lead to repositories that cause issues
+when used with nbgitpuller. Here are some recommendations to make your
+nbgitpuller experience smoother.
+
+
+## Never force push
+
+Never use `--force` or `--force-with-lease` when pushing to your repositories.
+This is general good git practice, and unless you have [fairly deep
+understanding](https://xkcd.com/1597/) of how git works, it might screw up some
+of your users' local repositories beyond repair.
+
+If you are using GitHub, you should enable [protected branches](https://docs.github.com/en/github/administering-a-repository/about-protected-branches)
+to prevent accidental force pushes.
+
+## Prevent your repos from becoming huge
+
+Larger git repos increase chances of timeouts and other intermittent failures
+that will be difficult to debug. They might leave your git repo in strange states
+too - contents fetched but not checked out, half-fetched, etc. Try and keep it small -
+under 100MB is great, under 1G is ok, but anything more is probably asking for trouble.
+
+Large datasets are the biggest reason for increasing repository sizes. Try distribute
+datasets some other way, use a subset of data, or compress your data if you need to.
+
+## Don't add `.ipynb_checkpoints` (and similar files) to your git repo
+
+Jupyter uses a hidden `.ipynb_checkpoints` directory to temporarily autosave copies of the
+notebook. If you accidentally commit your local computer's copy of this to the git repo,
+it can cause hard to debug issues when students click nbgitpuller links. The students'
+Jupyter Notebook servers in the JupyterHub will also generate `.ipynb_checkpoints` for
+autosaving, and conflicts between these two can cause issues. Similar issues can happen
+with other temporary, hidden files - like `.DS_Store`, `__pycache__`, etc.
+
+Adding `.ipynb_checkpoints` to your repo's `.gitignore` file will eliminate this
+class of issues completely. `git add` and similar commands will no longer
+accidentally include them in your repo. You can download this [python specific
+gitignore](https://github.com/github/gitignore/blob/master/Python.gitignore)
+file and put it in your repo as `.gitignore`, and it should take care of this.
diff --git a/nbgitpuller/docs/topic/url-options.rst b/nbgitpuller/docs/topic/url-options.rst
new file mode 100644
index 0000000000000000000000000000000000000000..200d3be14644885d265cb934e1fec9415d8387a3
--- /dev/null
+++ b/nbgitpuller/docs/topic/url-options.rst
@@ -0,0 +1,110 @@
+.. _topic/url-options:
+
+=============================
+Options in an nbgitpuller URL
+=============================
+
+.. note::
+
+ If you just want to generate an nbgitpuller link, we highly
+ recommend just using the `link generator `_
+
+Most aspects of the nbgitpuller student experience can be configured
+with various options in the nbgitpuller URL. This page documents
+the various options available, and their behavior.
+
+``repo``
+========
+
+The path to the git repository to be pulled from. This will accept
+any parameter that can be passed to a ``git clone`` command.
+
+``branch``
+==========
+
+Branch in the git repo to pull from. Defaults to ``master``.
+
+``urlpath``
+===========
+
+The URL to redirect the user to after synchronization has been complete. This
+URL is primarily used to open a specific file or directory in a specific
+application. This URL is interpreted relative to the base of the notebook
+server. The URL to be specified depends on the application you want
+the file to be opened in.
+
+.. warning::
+
+ ```` is relative to the directory the notebook
+ server was launched in - so the directory you see if you login to
+ JupyterHub regularly. This means you **must** include the name of
+ the local repository directory too, otherwise nbgitpuller can not
+ find the file.
+
+ 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 `_
+ takes care of all of this for you, so it is recommended to use that.
+
+
+Classic Jupyter Notebook
+------------------------
+
+To open a notebook, file or directory in the classic Jupyter Notebook
+interface, your pattern should be: ``/tree/``.
+
+JupyterLab
+----------
+
+To open a notebook, file or directory in the classic Jupyter Notebook
+interface, your pattern should be:
+``/lab/tree/%3Fautodecode``.
+
+The ``%3Fautodecode`` at the end makes sure you never get `a message
+`_ about needing to
+explicitly name a JupyterLab workspace.
+
+Shiny
+-----
+
+To open a directory containing `shiny `_ files,
+your pattern should be ``/shiny//``. The trailing
+slash is important.
+
+RStudio
+-------
+
+If you have RStudio installed and set up for use with your JupyterHub,
+you can pass ``/rstudio`` to ``urlpath`` to open RStudio after the
+repo has been pulled. You can not have RStudio open a specific file
+or directory, unfortunately.
+
+``depth``
+=========
+
+How deep to clone the git repo on initial pull. By default, the
+entire history of the git repository is pulled. This might be
+slow if your git repository is large. You can set this to 1 to
+pull only the latest commit on initial pull.
+
+Only explicitly set this if you are actively having performance
+problems.
+
+
+``targetPath``
+==============
+
+Where to place the repository when it is cloned.
+By default, Git repositories are cloned into the default working directory.
+You can specify a different parent directory for the clone by setting the environment variable ``NBGITPULLER_PARENTPATH``, this should be relative to the working directory.
+If you require full control over the destination directory, or want to set the directory at runtime in the nbgitpuller link use this parameter.
+
+
+Deprecated parameters
+=====================
+
+The following parameters are currently deprecated, and will be removed in
+a future version: ``subpath``, ``app``.
diff --git a/nbgitpuller/nbgitpuller/__init__.py b/nbgitpuller/nbgitpuller/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..b594128653e75cd874bd3627f3b841f821cb79e3
--- /dev/null
+++ b/nbgitpuller/nbgitpuller/__init__.py
@@ -0,0 +1,32 @@
+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 tornado.web import StaticFileHandler
+import os
+
+
+def _jupyter_server_extension_paths():
+ return [{
+ 'module': 'nbgitpuller',
+ }]
+
+
+def load_jupyter_server_extension(nbapp):
+ web_app = nbapp.web_app
+ base_url = url_path_join(web_app.settings['base_url'], 'git-pull')
+ handlers = [
+ (url_path_join(base_url, 'api'), SyncHandler),
+ (base_url, UIHandler),
+ (url_path_join(web_app.settings['base_url'], 'git-sync'), LegacyGitSyncRedirectHandler),
+ (url_path_join(web_app.settings['base_url'], 'interact'), LegacyInteractRedirectHandler),
+ (
+ url_path_join(base_url, 'static', '(.*)'),
+ StaticFileHandler,
+ {'path': os.path.join(os.path.dirname(__file__), 'static')}
+ )
+ ]
+ web_app.settings['nbapp'] = nbapp
+ web_app.add_handlers('.*', handlers)
+
+_load_jupyter_server_extension = load_jupyter_server_extension
diff --git a/nbgitpuller/nbgitpuller/etc/jupyter_notebook_config.d/nbgitpuller.json b/nbgitpuller/nbgitpuller/etc/jupyter_notebook_config.d/nbgitpuller.json
new file mode 100644
index 0000000000000000000000000000000000000000..ccfc3f5d72a9df6e576ab5027c15592da32a8000
--- /dev/null
+++ b/nbgitpuller/nbgitpuller/etc/jupyter_notebook_config.d/nbgitpuller.json
@@ -0,0 +1,8 @@
+{
+ "NotebookApp": {
+ "nbserver_extensions": {
+ "nbgitpuller": true
+ }
+ }
+}
+
diff --git a/nbgitpuller/nbgitpuller/etc/jupyter_server_config.d/nbgitpuller.json b/nbgitpuller/nbgitpuller/etc/jupyter_server_config.d/nbgitpuller.json
new file mode 100644
index 0000000000000000000000000000000000000000..e395847200431d8dade30bd8c5a3b43e10318c0f
--- /dev/null
+++ b/nbgitpuller/nbgitpuller/etc/jupyter_server_config.d/nbgitpuller.json
@@ -0,0 +1,8 @@
+{
+ "ServerApp": {
+ "jpserver_extensions": {
+ "nbgitpuller": true
+ }
+ }
+}
+
diff --git a/nbgitpuller/nbgitpuller/handlers.py b/nbgitpuller/nbgitpuller/handlers.py
new file mode 100644
index 0000000000000000000000000000000000000000..ae72afd9602caf160bc9ab17717d8262f90da79a
--- /dev/null
+++ b/nbgitpuller/nbgitpuller/handlers.py
@@ -0,0 +1,216 @@
+from tornado import gen, web, locks
+import traceback
+import urllib.parse
+
+from notebook.base.handlers import IPythonHandler
+import threading
+import json
+import os
+from queue import Queue, Empty
+import jinja2
+
+from .pull import GitPuller
+from .version import __version__
+from .wget import RequestRepoRawFile
+
+
+class SyncHandler(IPythonHandler):
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+
+ # We use this lock to make sure that only one sync operation
+ # can be happening at a time. Git doesn't like concurrent use!
+ if 'git_lock' not in self.settings:
+ self.settings['git_lock'] = locks.Lock()
+
+ @property
+ def git_lock(self):
+ return self.settings['git_lock']
+
+ @gen.coroutine
+ def emit(self, data):
+ if type(data) is not str:
+ serialized_data = json.dumps(data)
+ if 'output' in data:
+ self.log.info(data['output'].rstrip())
+ else:
+ serialized_data = data
+ self.log.info(data)
+ self.write('data: {}\n\n'.format(serialized_data))
+ yield self.flush()
+
+ @web.authenticated
+ @gen.coroutine
+ def get(self):
+ try:
+ yield self.git_lock.acquire(1)
+ except gen.TimeoutError:
+ self.emit({
+ 'phase': 'error',
+ 'message': 'Another git operations is currently running, try again in a few minutes'
+ })
+ return
+
+ try:
+ repo = self.get_argument('repo')
+ branch = self.get_argument('branch', None)
+ depth = self.get_argument('depth', None)
+ if depth:
+ depth = int(depth)
+ # The default working directory is the directory from which Jupyter
+ # server is launched, which is not the same as the root notebook
+ # directory assuming either --notebook-dir= is used from the
+ # command line or c.NotebookApp.notebook_dir is set in the jupyter
+ # configuration. This line assures that all repos are cloned
+ # relative to server_root_dir/,
+ # so that all repos are always in scope after cloning. Sometimes
+ # server_root_dir will include things like `~` and so the path
+ # must be expanded.
+ repo_parent_dir = os.path.join(os.path.expanduser(self.settings['server_root_dir']),
+ os.getenv('NBGITPULLER_PARENTPATH', ''))
+ repo_dir = os.path.join(repo_parent_dir, self.get_argument('targetpath', repo.split('/')[-1]))
+
+ # We gonna send out event streams!
+ self.set_header('content-type', 'text/event-stream')
+ self.set_header('cache-control', 'no-cache')
+
+ gp = GitPuller(repo, repo_dir, branch=branch, depth=depth, parent=self.settings['nbapp'])
+
+ q = Queue()
+
+ def pull():
+ try:
+ for line in gp.pull():
+ q.put_nowait(line)
+ # Sentinel when we're done
+ q.put_nowait(None)
+ except Exception as e:
+ q.put_nowait(e)
+ raise e
+ self.gp_thread = threading.Thread(target=pull)
+
+ self.gp_thread.start()
+
+ while True:
+ try:
+ progress = q.get_nowait()
+ except Empty:
+ yield gen.sleep(0.5)
+ continue
+ if progress is None:
+ break
+ if isinstance(progress, Exception):
+ self.emit({
+ 'phase': 'error',
+ 'message': str(progress),
+ 'output': '\n'.join([
+ line.strip()
+ for line in traceback.format_exception(
+ type(progress), progress, progress.__traceback__
+ )
+ ])
+ })
+ return
+
+ self.emit({'output': progress, 'phase': 'syncing'})
+
+ self.emit({'phase': 'finished'})
+ except Exception as e:
+ self.emit({
+ 'phase': 'error',
+ 'message': str(e),
+ 'output': '\n'.join([
+ line.strip()
+ for line in traceback.format_exception(
+ type(e), e, e.__traceback__
+ )
+ ])
+ })
+ finally:
+ self.git_lock.release()
+
+
+class UIHandler(IPythonHandler):
+ def initialize(self):
+ super().initialize()
+ # FIXME: Is this really the best way to use jinja2 here?
+ # I can't seem to get the jinja2 env in the base handler to
+ # actually load templates from arbitrary paths ugh.
+ jinja2_env = self.settings['jinja2_env']
+ jinja2_env.loader = jinja2.ChoiceLoader([
+ jinja2_env.loader,
+ jinja2.FileSystemLoader(
+ os.path.join(os.path.dirname(__file__), 'templates')
+ )
+ ])
+
+ @web.authenticated
+ @gen.coroutine
+ def get(self):
+ app_env = os.getenv('NBGITPULLER_APP', default='notebook')
+
+ repo = self.get_argument('repo')
+ branch = self.get_argument('branch', None)
+ depth = self.get_argument('depth', None)
+ urlPath = self.get_argument('urlpath', None) or \
+ self.get_argument('urlPath', None)
+ subPath = self.get_argument('subpath', None) or \
+ self.get_argument('subPath', '.')
+ app = self.get_argument('app', app_env)
+ parent_reldir = os.getenv('NBGITPULLER_PARENTPATH', '')
+ targetpath = self.get_argument('targetpath', None) or \
+ self.get_argument('targetPath', repo.split('/')[-1])
+ if(urlPath.endswith('.ipynb')):
+ # 添加获取文件 和跳转的逻辑
+ rrrf = RequestRepoRawFile()
+ path = rrrf.wgetFile(repo, branch, urlPath)
+ else:
+ if urlPath:
+ path = urlPath
+ else:
+ path = os.path.join(parent_reldir, targetpath, subPath)
+ if app.lower() == 'lab':
+ path = 'lab/tree/' + path
+ elif path.lower().endswith('.ipynb'):
+ path = 'notebooks/' + path
+ else:
+ path = 'tree/' + path
+
+ self.write(
+ self.render_template(
+ 'status.html',
+ repo=repo, branch=branch, path=path, depth=depth, targetpath=targetpath, version=__version__
+ ))
+ self.flush()
+
+
+class LegacyGitSyncRedirectHandler(IPythonHandler):
+ @web.authenticated
+ @gen.coroutine
+ def get(self):
+ new_url = '{base}git-pull?{query}'.format(
+ base=self.base_url,
+ query=self.request.query
+ )
+ self.redirect(new_url)
+
+
+class LegacyInteractRedirectHandler(IPythonHandler):
+ @web.authenticated
+ @gen.coroutine
+ def get(self):
+ repo = self.get_argument('repo')
+ account = self.get_argument('account', 'data-8')
+ repo_url = 'https://github.com/{account}/{repo}'.format(account=account, repo=repo)
+ query = {
+ 'repo': repo_url,
+ # branch & subPath are optional
+ 'branch': self.get_argument('branch', 'gh-pages'),
+ 'subPath': self.get_argument('path', '.')
+ }
+ new_url = '{base}git-pull?{query}'.format(
+ base=self.base_url,
+ query=urllib.parse.urlencode(query)
+ )
+
+ self.redirect(new_url)
diff --git a/nbgitpuller/nbgitpuller/pull.py b/nbgitpuller/nbgitpuller/pull.py
new file mode 100644
index 0000000000000000000000000000000000000000..cc18ac97ae48615c566705d4d659f3250b7705d4
--- /dev/null
+++ b/nbgitpuller/nbgitpuller/pull.py
@@ -0,0 +1,319 @@
+import os
+import subprocess
+import logging
+import time
+import argparse
+import datetime
+from traitlets import Integer, default
+from traitlets.config import Configurable
+from functools import partial
+
+
+def execute_cmd(cmd, **kwargs):
+ """
+ Call given command, yielding output line by line
+ """
+ yield '$ {}\n'.format(' '.join(cmd))
+ kwargs['stdout'] = subprocess.PIPE
+ kwargs['stderr'] = subprocess.STDOUT
+
+ proc = subprocess.Popen(cmd, **kwargs)
+
+ # Capture output for logging.
+ # Each line will be yielded as text.
+ # This should behave the same as .readline(), but splits on `\r` OR `\n`,
+ # not just `\n`.
+ buf = []
+
+ def flush():
+ line = b''.join(buf).decode('utf8', 'replace')
+ buf[:] = []
+ return line
+
+ c_last = ''
+ try:
+ for c in iter(partial(proc.stdout.read, 1), b''):
+ if c_last == b'\r' and buf and c != b'\n':
+ yield flush()
+ buf.append(c)
+ if c == b'\n':
+ yield flush()
+ c_last = c
+ finally:
+ ret = proc.wait()
+ if ret != 0:
+ raise subprocess.CalledProcessError(ret, cmd)
+
+
+class GitPuller(Configurable):
+ depth = Integer(
+ config=True,
+ help="""
+ Depth (ie, commit count) of clone operations. Set this to 0 to make a
+ full depth clone.
+
+ Defaults to the value of the environment variable NBGITPULLER_DEPTH, or
+ 1 if the the environment variable isn't set.
+ """
+ )
+
+ @default('depth')
+ def _depth_default(self):
+ """This is a workaround for setting the same default directly in the
+ definition of the traitlet above. Without it, the test fails because a
+ change in the environment variable has no impact. I think this is a
+ consequence of the tests not starting with a totally clean environment
+ where the GitPuller class hadn't been loaded already."""
+ return int(os.environ.get('NBGITPULLER_DEPTH', 1))
+
+ def __init__(self, git_url, repo_dir, **kwargs):
+ assert git_url
+
+ self.git_url = git_url
+ self.branch_name = kwargs.pop("branch")
+
+ if self.branch_name is None:
+ self.branch_name = self.resolve_default_branch()
+ elif not self.branch_exists(self.branch_name):
+ raise ValueError(f"Branch: {self.branch_name} -- not found in repo: {self.git_url}")
+
+ self.repo_dir = repo_dir
+ newargs = {k: v for k, v in kwargs.items() if v is not None}
+ super(GitPuller, self).__init__(**newargs)
+
+ def branch_exists(self, branch):
+ """
+ This checks to make sure the branch we are told to access
+ exists in the repo
+ """
+ try:
+ heads = subprocess.run(
+ ["git", "ls-remote", "--heads", "--", self.git_url],
+ capture_output=True,
+ text=True,
+ check=True
+ )
+ tags = subprocess.run(
+ ["git", "ls-remote", "--tags", "--", self.git_url],
+ capture_output=True,
+ text=True,
+ check=True
+ )
+ lines = heads.stdout.splitlines() + tags.stdout.splitlines()
+ branches = []
+ for line in lines:
+ _, ref = line.split()
+ refs, heads, branch_name = ref.split("/", 2)
+ branches.append(branch_name)
+ return branch in branches
+ except subprocess.CalledProcessError:
+ m = f"Problem accessing list of branches and/or tags: {self.git_url}"
+ logging.exception(m)
+ raise ValueError(m)
+
+ def resolve_default_branch(self):
+ """
+ This will resolve the default branch of the repo in
+ the case where the branch given does not exist
+ """
+ try:
+ head_branch = subprocess.run(
+ ["git", "ls-remote", "--symref", "--", self.git_url, "HEAD"],
+ capture_output=True,
+ text=True,
+ check=True
+ )
+ for line in head_branch.stdout.splitlines():
+ if line.startswith("ref:"):
+ # line resembles --> ref: refs/heads/main HEAD
+ _, ref, head = line.split()
+ refs, heads, branch_name = ref.split("/", 2)
+ return branch_name
+ raise ValueError(f"default branch not found in {self.git_url}")
+ except subprocess.CalledProcessError:
+ m = f"Problem accessing HEAD branch: {self.git_url}"
+ logging.exception(m)
+ raise ValueError(m)
+
+ def pull(self):
+ """
+ Pull selected repo from a remote git repository,
+ while preserving user changes
+ """
+ if not os.path.exists(self.repo_dir):
+ yield from self.initialize_repo()
+ else:
+ yield from self.update()
+
+ def initialize_repo(self):
+ """
+ Clones repository
+ """
+ logging.info('Repo {} doesn\'t exist. Cloning...'.format(self.repo_dir))
+ clone_args = ['git', 'clone']
+ if self.depth and self.depth > 0:
+ clone_args.extend(['--depth', str(self.depth)])
+ clone_args.extend(['--branch', self.branch_name])
+ clone_args.extend(["--", self.git_url, self.repo_dir])
+ yield from execute_cmd(clone_args)
+ logging.info('Repo {} initialized'.format(self.repo_dir))
+
+ def reset_deleted_files(self):
+ """
+ Runs the equivalent of git checkout -- for each file that was
+ deleted. This allows us to delete a file, hit an interact link, then get a
+ clean version of the file again.
+ """
+
+ yield from self.ensure_lock()
+ deleted_files = subprocess.check_output([
+ 'git', 'ls-files', '--deleted', '-z'
+ ], cwd=self.repo_dir).decode().strip().split('\0')
+
+ for filename in deleted_files:
+ if filename: # Filter out empty lines
+ yield from execute_cmd(['git', 'checkout', 'origin/{}'.format(self.branch_name), '--', filename], cwd=self.repo_dir)
+
+ def repo_is_dirty(self):
+ """
+ Return true if repo is dirty
+ """
+ try:
+ subprocess.check_call(['git', 'diff-files', '--quiet'], cwd=self.repo_dir)
+ # Return code is 0
+ return False
+ except subprocess.CalledProcessError:
+ return True
+
+ def update_remotes(self):
+ """
+ Do a git fetch so our remotes are up to date
+ """
+ yield from execute_cmd(['git', 'fetch'], cwd=self.repo_dir)
+
+ def find_upstream_changed(self, kind):
+ """
+ Return list of files that have been changed upstream belonging to a particular kind of change
+ """
+ output = subprocess.check_output([
+ 'git', 'log', '..origin/{}'.format(self.branch_name),
+ '--oneline', '--name-status'
+ ], cwd=self.repo_dir).decode()
+ files = []
+ for line in output.split('\n'):
+ if line.startswith(kind):
+ files.append(os.path.join(self.repo_dir, line.split('\t', 1)[1]))
+
+ return files
+
+ def ensure_lock(self):
+ """
+ Make sure we have the .git/lock required to do modifications on the repo
+
+ This must be called before any git commands that modify state. This isn't guaranteed
+ to be atomic, due to the nature of using files for locking. But it's the best we
+ can do right now.
+ """
+ try:
+ lockpath = os.path.join(self.repo_dir, '.git', 'index.lock')
+ mtime = os.path.getmtime(lockpath)
+ # A lock file does exist
+ # If it's older than 10 minutes, we just assume it is stale and take over
+ # If not, we fail with an explicit error.
+ if time.time() - mtime > 600:
+ yield "Stale .git/index.lock found, attempting to remove"
+ os.remove(lockpath)
+ yield "Stale .git/index.lock removed"
+ else:
+ raise Exception('Recent .git/index.lock found, operation can not proceed. Try again in a few minutes.')
+ except FileNotFoundError:
+ # No lock is held by other processes, we are free to go
+ return
+
+ def rename_local_untracked(self):
+ """
+ Rename local untracked files that would require pulls
+ """
+ # Find what files have been added!
+ new_upstream_files = self.find_upstream_changed('A')
+ for f in new_upstream_files:
+ if os.path.exists(f):
+ # If there's a file extension, put the timestamp before that
+ ts = datetime.datetime.now().strftime('__%Y%m%d%H%M%S')
+ path_head, path_tail = os.path.split(f)
+ path_tail = ts.join(os.path.splitext(path_tail))
+ new_file_name = os.path.join(path_head, path_tail)
+ os.rename(f, new_file_name)
+ yield 'Renamed {} to {} to avoid conflict with upstream'.format(f, new_file_name)
+
+ def update(self):
+ """
+ Do the pulling if necessary
+ """
+ # Fetch remotes, so we know we're dealing with latest remote
+ yield from self.update_remotes()
+
+ # Rename local untracked files that might be overwritten by pull
+ yield from self.rename_local_untracked()
+
+ # Reset local files that have been deleted. We don't actually expect users to
+ # delete something that's present upstream and expect to keep it. This prevents
+ # unnecessary conflicts, and also allows users to click the link again to get
+ # a fresh copy of a file they might have screwed up.
+ yield from self.reset_deleted_files()
+
+ # If there are local changes, make a commit so we can do merges when pulling
+ # We also allow empty commits. On NFS (at least), sometimes repo_is_dirty returns a false
+ # positive, returning True even when there are no local changes (git diff-files seems to return
+ # bogus output?). While ideally that would not happen, allowing empty commits keeps us
+ # resilient to that issue.
+ # We explicitly set user info of the commits we are making, to keep that separate from
+ # whatever author info is set in system / repo config by the user. We pass '-c' to git
+ # itself (rather than to 'git commit') to temporarily set config variables. This is
+ # better than passing --author, since git treats author separately from committer.
+ if self.repo_is_dirty():
+ yield from self.ensure_lock()
+ yield from execute_cmd([
+ 'git',
+ '-c', 'user.email=nbgitpuller@nbgitpuller.link',
+ '-c', 'user.name=nbgitpuller',
+ 'commit',
+ '-am', 'Automatic commit by nbgitpuller',
+ '--allow-empty'
+ ], cwd=self.repo_dir)
+
+ # Merge master into local!
+ yield from self.ensure_lock()
+ yield from execute_cmd([
+ 'git',
+ '-c', 'user.email=nbgitpuller@nbgitpuller.link',
+ '-c', 'user.name=nbgitpuller',
+ 'merge',
+ '-Xours', 'origin/{}'.format(self.branch_name)
+ ], cwd=self.repo_dir)
+
+
+def main():
+ """
+ Synchronizes a github repository with a local repository.
+ """
+ logging.basicConfig(
+ format='[%(asctime)s] %(levelname)s -- %(message)s',
+ level=logging.DEBUG)
+
+ parser = argparse.ArgumentParser(description='Synchronizes a github repository with a local repository.')
+ parser.add_argument('git_url', help='Url of the repo to sync')
+ parser.add_argument('branch_name', default=None, help='Branch of repo to sync', nargs='?')
+ parser.add_argument('repo_dir', default='.', help='Path to clone repo under', nargs='?')
+ args = parser.parse_args()
+
+ for line in GitPuller(
+ args.git_url,
+ args.repo_dir,
+ branch=args.branch_name if args.branch_name else None
+ ).pull():
+ print(line)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/nbgitpuller/nbgitpuller/static/js/index.js b/nbgitpuller/nbgitpuller/static/js/index.js
new file mode 100644
index 0000000000000000000000000000000000000000..6b2f7ac04825c6b46d2d40aa7621805a03ee6475
--- /dev/null
+++ b/nbgitpuller/nbgitpuller/static/js/index.js
@@ -0,0 +1,297 @@
+import { Terminal } from 'xterm';
+import { FitAddon } from 'xterm-addon-fit';
+import css from '../../../node_modules/xterm/css/xterm.css';
+
+function GitSync(baseUrl, repo, branch, depth, targetpath, path) {
+ // Class that talks to the API backend & emits events as appropriate
+ this.baseUrl = baseUrl;
+ this.repo = repo;
+ this.branch = branch;
+ this.depth = depth;
+ this.targetpath = targetpath;
+ this.redirectUrl = baseUrl + path;
+
+ this.callbacks = {};
+}
+
+GitSync.prototype.addHandler = function(event, cb) {
+ if (this.callbacks[event] == undefined) {
+ this.callbacks[event] = [cb];
+ } else {
+ this.callbacks[event].push(cb);
+ }
+};
+
+GitSync.prototype._emit = function(event, data) {
+ if (this.callbacks[event] == undefined) { return; }
+ $.each(this.callbacks[event], function(i, ev) {
+ ev(data);
+ });
+};
+
+
+GitSync.prototype.start = function() {
+ if (this.path && this.path.endsWith('.ipynb')){
+ that._emit('finished');
+ }else{
+ // Start git pulling handled by SyncHandler, declared in handlers.py
+ var syncUrlParams = {
+ repo: this.repo,
+ targetpath: this.targetpath
+ }
+ if (typeof this.depth !== 'undefined' && this.depth != undefined) {
+ syncUrlParams['depth'] = this.depth;
+ }
+ if (typeof this.branch !== 'undefined' && this.branch != undefined) {
+ syncUrlParams['branch'] = this.branch;
+ }
+ var syncUrl = this.baseUrl + 'git-pull/api?' + $.param(syncUrlParams);
+
+ this.eventSource = new EventSource(syncUrl);
+ var that = this;
+ this.eventSource.addEventListener('message', function(ev) {
+ var data = JSON.parse(ev.data);
+ if (data.phase == 'finished' || data.phase == 'error') {
+ that.eventSource.close();
+ }
+ that._emit(data.phase, data);
+ });
+ this.eventSource.addEventListener('error', function(error) {
+ console.log(arguments);
+ that._emit('error', error);
+ });
+ }
+};
+
+function GitSyncView(termSelector, progressSelector, termToggleSelector) {
+ // Class that encapsulates view rendering as much as possible
+ this.term = new Terminal({
+ convertEol: true
+ });
+ this.fit = new FitAddon();
+ this.term.loadAddon(this.fit);
+
+ this.visible = false;
+ this.$progress = $(progressSelector);
+
+ this.$termToggle = $(termToggleSelector);
+ this.termSelector = termSelector;
+
+ var that = this;
+ this.$termToggle.click(function() {
+ that.setTerminalVisibility(!that.visible);
+ });
+}
+
+GitSyncView.prototype.setTerminalVisibility = function(visible) {
+ if (visible) {
+ $(this.termSelector).parent().removeClass('hidden');
+ } else {
+ $(this.termSelector).parent().addClass('hidden');
+ }
+ this.visible = visible;
+ if (visible) {
+ // See https://github.com/jupyterhub/nbgitpuller/pull/46 on why this is here.
+ if (!this.term.element) {
+ this.term.open($(this.termSelector)[0]);
+ }
+ this.fit.fit();
+ }
+
+}
+
+GitSyncView.prototype.setProgressValue = function(val) {
+ this.$progress.attr('aria-valuenow', val);
+ this.$progress.css('width', val + '%');
+};
+
+GitSyncView.prototype.getProgressValue = function() {
+ return parseFloat(this.$progress.attr('aria-valuenow'));
+};
+
+GitSyncView.prototype.setProgressText = function(text) {
+ this.$progress.children('span').text(text);
+};
+
+GitSyncView.prototype.getProgressText = function() {
+ return this.$progress.children('span').text();
+};
+
+GitSyncView.prototype.setProgressError = function(isError) {
+ if (isError) {
+ this.$progress.addClass('progress-bar-danger');
+ } else {
+ this.$progress.removeClass('progress-bar-danger');
+ }
+};
+
+var get_body_data = function(key) {
+ /**
+ * get a url-encoded item from body.data and decode it
+ * we should never have any encoded URLs anywhere else in code
+ * until we are building an actual request
+ */
+ var val = $('body').data(key);
+ if (typeof val === 'undefined')
+ return val;
+ return decodeURIComponent(val);
+};
+
+var gs = new GitSync(
+ get_body_data('baseUrl'),
+ get_body_data('repo'),
+ get_body_data('branch'),
+ get_body_data('depth'),
+ get_body_data('targetpath'),
+ get_body_data('path')
+);
+
+var gsv = new GitSyncView(
+ '#status-details',
+ '#status-panel-title',
+ '#status-panel-toggle'
+);
+
+gs.addHandler('syncing', function(data) {
+ gsv.term.write(data.output);
+});
+gs.addHandler('finished', function(data) {
+ progressTimers.forEach(function(timer) { clearInterval(timer); });
+ gsv.setProgressValue(100);
+ gsv.setProgressText('Sync finished, redirecting...');
+ window.location.href = gs.redirectUrl;
+});
+gs.addHandler('error', function(data) {
+ progressTimers.forEach(function(timer) { clearInterval(timer); });
+ gsv.setProgressValue(100);
+ gsv.setProgressText('Error: ' + data.message);
+ gsv.setProgressError(true);
+ gsv.setTerminalVisibility(true);
+ if (data.output) {
+ gsv.term.write(data.output);
+ }
+});
+gs.start();
+
+$('#header, #site').show();
+
+// Make sure we provide plenty of appearances of progress!
+var progressTimers = [];
+progressTimers.push(setInterval(function() {
+ gsv.setProgressText(substatus_messages[Math.floor(Math.random() * substatus_messages.length)]);
+}, 3000));
+progressTimers.push(setInterval(function() {
+ gsv.setProgressText(gsv.getProgressText() + '.');
+}, 800));
+
+progressTimers.push(setInterval(function() {
+ // Illusion of progress!
+ gsv.setProgressValue(gsv.getProgressValue() + (0.01 * (100 - gsv.getProgressValue())));
+}, 900));
+
+
+var substatus_messages = [
+ "Adding Hidden Agendas",
+ "Adjusting Bell Curves",
+ "Aesthesizing Industrial Areas",
+ "Aligning Covariance Matrices",
+ "Applying Feng Shui Shaders",
+ "Applying Theatre Soda Layer",
+ "Asserting Packed Exemplars",
+ "Attempting to Lock Back-Buffer",
+ "Binding Sapling Root System",
+ "Breeding Fauna",
+ "Building Data Trees",
+ "Bureacritizing Bureaucracies",
+ "Calculating Inverse Probability Matrices",
+ "Calculating Llama Expectoration Trajectory",
+ "Calibrating Blue Skies",
+ "Charging Ozone Layer",
+ "Coalescing Cloud Formations",
+ "Cohorting Exemplars",
+ "Collecting Meteor Particles",
+ "Compounding Inert Tessellations",
+ "Compressing Fish Files",
+ "Computing Optimal Bin Packing",
+ "Concatenating Sub-Contractors",
+ "Containing Existential Buffer",
+ "Debarking Ark Ramp",
+ "Debunching Unionized Commercial Services",
+ "Deciding What Message to Display Next",
+ "Decomposing Singular Values",
+ "Decrementing Tectonic Plates",
+ "Deleting Ferry Routes",
+ "Depixelating Inner Mountain Surface Back Faces",
+ "Depositing Slush Funds",
+ "Destabilizing Economic Indicators",
+ "Determining Width of Blast Fronts",
+ "Dicing Models",
+ "Diluting Livestock Nutrition Variables",
+ "Downloading Satellite Terrain Data",
+ "Eating Ice Cream",
+ "Exposing Flash Variables to Streak System",
+ "Extracting Resources",
+ "Factoring Pay Scale",
+ "Fixing Election Outcome Matrix",
+ "Flood-Filling Ground Water",
+ "Flushing Pipe Network",
+ "Gathering Particle Sources",
+ "Generating Jobs",
+ "Gesticulating Mimes",
+ "Graphing Whale Migration",
+ "Hiding Willio Webnet Mask",
+ "Implementing Impeachment Routine",
+ "Increasing Accuracy of RCI Simulators",
+ "Increasing Magmafacation",
+ "Initializing Rhinoceros Breeding Timetable",
+ "Initializing Robotic Click-Path AI",
+ "Inserting Sublimated Messages",
+ "Integrating Curves",
+ "Integrating Illumination Form Factors",
+ "Integrating Population Graphs",
+ "Iterating Cellular Automata",
+ "Lecturing Errant Subsystems",
+ "Modeling Object Components",
+ "Normalizing Power",
+ "Obfuscating Quigley Matrix",
+ "Overconstraining Dirty Industry Calculations",
+ "Partitioning City Grid Singularities",
+ "Perturbing Matrices",
+ "Polishing Water Highlights",
+ "Populating Lot Templates",
+ "Preparing Sprites for Random Walks",
+ "Prioritizing Landmarks",
+ "Projecting Law Enforcement Pastry Intake",
+ "Realigning Alternate Time Frames",
+ "Reconfiguring User Mental Processes",
+ "Relaxing Splines",
+ "Removing Road Network Speed Bumps",
+ "Removing Texture Gradients",
+ "Removing Vehicle Avoidance Behavior",
+ "Resolving GUID Conflict",
+ "Reticulating Splines",
+ "Retracting Phong Shader",
+ "Retrieving from Back Store",
+ "Reverse Engineering Image Consultant",
+ "Routing Neural Network Infanstructure",
+ "Scattering Rhino Food Sources",
+ "Scrubbing Terrain",
+ "Searching for Llamas",
+ "Seeding Architecture Simulation Parameters",
+ "Sequencing Particles",
+ "Setting Advisor Moods",
+ "Setting Inner Deity Indicators",
+ "Setting Universal Physical Constants",
+ "Smashing The Patriarchy",
+ "Sonically Enhancing Occupant-Free Timber",
+ "Speculating Stock Market Indices",
+ "Splatting Transforms",
+ "Stratifying Ground Layers",
+ "Sub-Sampling Water Data",
+ "Synthesizing Gravity",
+ "Synthesizing Wavelets",
+ "Time-Compressing Simulator Clock",
+ "Unable to Reveal Current Activity",
+ "Weathering Buildings",
+ "Zeroing Crime Network"
+];
diff --git a/nbgitpuller/nbgitpuller/templates/page.html b/nbgitpuller/nbgitpuller/templates/page.html
new file mode 100644
index 0000000000000000000000000000000000000000..aae54ad5dcf4e51ce6c0d423402b5b4fbd3b1031
--- /dev/null
+++ b/nbgitpuller/nbgitpuller/templates/page.html
@@ -0,0 +1,89 @@
+
+
+
+
+
+ {% block title %}Jupyter Server{% endblock %}
+ {% block favicon %}{% endblock %}
+
+
+
+
+
+
+ {% block stylesheet %}
+ {% endblock stylesheet %}
+
+ {% block meta %}
+ {% endblock meta %}
+
+
+
+
+
+
+