├── style
├── index.css
├── index.js
├── octocat_error.png
├── octocat-light.svg
├── octocat-dark.svg
├── binder.svg
└── base.css
├── setup.py
├── .eslintignore
├── .yarnrc.yml
├── gitception.png
├── src
├── svg.d.ts
├── index.ts
├── github.ts
├── browser.ts
└── contents.ts
├── .prettierignore
├── .prettierrc
├── jupyter-config
├── jupyter_server_config.d
│ └── jupyterlab_github.json
└── jupyter_notebook_config.d
│ └── jupyterlab_github.json
├── RELEASE.md
├── install.json
├── .stylelintrc
├── .travis.yml
├── binder
├── environment.yml
└── postBuild
├── tsconfig.json
├── jupyterlab_github
├── api
│ └── api.yaml
└── __init__.py
├── schema
└── drive.json
├── .github
└── workflows
│ ├── check-release.yml
│ ├── build.yml
│ ├── publish-changelog.yml
│ ├── binder-on-pr.yaml
│ ├── prep-release.yml
│ ├── publish-release.yml
│ └── packaging.yml
├── LICENSE
├── .gitignore
├── pyproject.toml
├── package.json
├── README.md
├── .eslintrc.js
└── CHANGELOG.md
/style/index.css:
--------------------------------------------------------------------------------
1 | @import 'base.css';
2 |
--------------------------------------------------------------------------------
/style/index.js:
--------------------------------------------------------------------------------
1 | import './base.css';
2 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | __import__('setuptools').setup()
2 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | dist
3 | coverage
4 | **/*.d.ts
5 | tests
6 |
--------------------------------------------------------------------------------
/.yarnrc.yml:
--------------------------------------------------------------------------------
1 | nodeLinker: node-modules
2 | enableImmutableInstalls: false
3 |
--------------------------------------------------------------------------------
/gitception.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jupyterlab/jupyterlab-github/HEAD/gitception.png
--------------------------------------------------------------------------------
/src/svg.d.ts:
--------------------------------------------------------------------------------
1 | declare module '*.svg' {
2 | const image: string;
3 | export default image;
4 | }
5 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | **/node_modules
3 | **/lib
4 | **/package.json
5 | jupyterlab_github
6 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "singleQuote": true,
3 | "trailingComma": "none",
4 | "arrowParens": "avoid"
5 | }
6 |
--------------------------------------------------------------------------------
/style/octocat_error.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jupyterlab/jupyterlab-github/HEAD/style/octocat_error.png
--------------------------------------------------------------------------------
/jupyter-config/jupyter_server_config.d/jupyterlab_github.json:
--------------------------------------------------------------------------------
1 | {
2 | "ServerApp": {
3 | "jpserver_extensions": {
4 | "jupyterlab_github": true
5 | }
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/jupyter-config/jupyter_notebook_config.d/jupyterlab_github.json:
--------------------------------------------------------------------------------
1 | {
2 | "NotebookApp": {
3 | "nbserver_extensions": {
4 | "jupyterlab_github": true
5 | }
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/RELEASE.md:
--------------------------------------------------------------------------------
1 | # Making a Release
2 |
3 | The recommended way to make a release is to use [`jupyter_releaser`](https://jupyter-releaser.readthedocs.io/en/latest/get_started/making_release_from_repo.html).
4 |
--------------------------------------------------------------------------------
/install.json:
--------------------------------------------------------------------------------
1 | {
2 | "packageManager": "python",
3 | "packageName": "jupyterlab_github",
4 | "uninstallInstructions": "Use your Python package manager (pip, conda, etc.) to uninstall the package jupyterlab_github"
5 | }
6 |
--------------------------------------------------------------------------------
/.stylelintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": [
3 | "stylelint-config-recommended",
4 | "stylelint-config-standard",
5 | "stylelint-prettier/recommended"
6 | ],
7 | "rules": {
8 | "no-empty-source": null,
9 | "selector-class-pattern": null,
10 | "property-no-vendor-prefix": null,
11 | "selector-no-vendor-prefix": null,
12 | "value-no-vendor-prefix": null
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - '14'
4 | sudo: false
5 | notifications:
6 | email: false
7 | before_install:
8 | - wget https://repo.continuum.io/miniconda/Miniconda3-latest-Linux-x86_64.sh -O ~/miniconda.sh;
9 | - bash ~/miniconda.sh -b -p $HOME/miniconda
10 | - export PATH="$HOME/miniconda/bin:$PATH"
11 | - pip install jupyterlab
12 | install:
13 | - jlpm install
14 | - jlpm build
15 | - jupyter labextension install .
16 | script:
17 | - echo "Build successful"
18 |
--------------------------------------------------------------------------------
/binder/environment.yml:
--------------------------------------------------------------------------------
1 | # a mybinder.org-ready environment for demoing jupyterlab-github
2 | # this environment may also be used locally on Linux/MacOS/Windows, e.g.
3 | #
4 | # conda env update --file binder/environment.yml
5 | # conda activate jupyterlab-github-demo
6 | #
7 | name: jupyterlab-github-demo
8 |
9 | channels:
10 | - conda-forge
11 |
12 | dependencies:
13 | # runtime dependencies
14 | - python >=3.8,<3.11.0
15 | - jupyterlab >=4,<5.0.0a0
16 | # labextension build dependencies
17 | - nodejs >=18,<19
18 | - pip
19 | - wheel
20 | # additional packages for demos
21 | # - ipywidgets
22 |
23 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "declaration": true,
4 | "noImplicitAny": true,
5 | "strictNullChecks": true,
6 | "noEmitOnError": true,
7 | "noUnusedLocals": true,
8 | "module": "esnext",
9 | "moduleResolution": "node",
10 | "target": "es2018",
11 | "outDir": "lib",
12 | "allowSyntheticDefaultImports": true,
13 | "composite": true,
14 | "esModuleInterop": true,
15 | "incremental": true,
16 | "preserveWatchOutput": true,
17 | "resolveJsonModule": true,
18 | "rootDir": "src",
19 | "strict": true,
20 | "types": [],
21 | "jsx": "react"
22 | },
23 | "include": ["src/*"]
24 | }
25 |
--------------------------------------------------------------------------------
/jupyterlab_github/api/api.yaml:
--------------------------------------------------------------------------------
1 | openapi: 3.0.0
2 | info:
3 | title: JupyterLab GitHub proxy
4 | description: Proxies GitHub API requests from JupyterLab to GitHub, optionally adding credentials to the request.
5 | version: 0.1.0
6 |
7 | paths:
8 | /github/{apiPath}:
9 | get:
10 | summary: Gets the resource at the apiPath for the GitHub API v3.
11 | parameters:
12 | - name: apiPath
13 | in: path
14 | required: true
15 | description: API path for GitHub v3.
16 | schema:
17 | type: string
18 | format: uri
19 | responses:
20 | '200':
21 | description: OK
22 | '404':
23 | description: Not found
24 | '403':
25 | description: Not authorized
26 |
--------------------------------------------------------------------------------
/schema/drive.json:
--------------------------------------------------------------------------------
1 | {
2 | "jupyter.lab.setting-icon-class": "jp-GitHub-icon",
3 | "jupyter.lab.setting-icon-label": "GitHub",
4 | "title": "GitHub",
5 | "description": "Settings for the GitHub plugin.",
6 | "properties": {
7 | "baseUrl": {
8 | "type": "string",
9 | "title": "The GitHub Base URL",
10 | "default": "https://github.com"
11 | },
12 | "accessToken": {
13 | "type": "string",
14 | "title": "A GitHub Personal Access Token",
15 | "description": "WARNING: For security reasons access tokens should be set in the server extension.",
16 | "default": ""
17 | },
18 | "defaultRepo": {
19 | "type": "string",
20 | "title": "Default Repository",
21 | "default": ""
22 | }
23 | },
24 | "type": "object"
25 | }
26 |
--------------------------------------------------------------------------------
/.github/workflows/check-release.yml:
--------------------------------------------------------------------------------
1 | name: Check Release
2 | on:
3 | push:
4 | branches: [ main ]
5 | pull_request:
6 | branches: ["*"]
7 |
8 | jobs:
9 | check_release:
10 | runs-on: ubuntu-latest
11 |
12 | steps:
13 | - name: Checkout
14 | uses: actions/checkout@v3
15 |
16 | - name: Base Setup
17 | uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1
18 |
19 | - name: Check Release
20 | uses: jupyter-server/jupyter_releaser/.github/actions/check-release@v2
21 | with:
22 | token: ${{ secrets.GITHUB_TOKEN }}
23 |
24 | - name: Upload Distributions
25 | uses: actions/upload-artifact@v3
26 | with:
27 | name: jupyterlab_github-releaser-dist-${{ github.run_number }}
28 | path: .jupyter_releaser_checkout/dist
29 |
--------------------------------------------------------------------------------
/style/octocat-light.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.github/workflows/build.yml:
--------------------------------------------------------------------------------
1 | name: Build
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 | pull_request:
8 | branches:
9 | - main
10 |
11 | defaults:
12 | run:
13 | shell: bash -l {0}
14 |
15 | jobs:
16 | build:
17 | runs-on: ubuntu-latest
18 |
19 | steps:
20 | - name: Checkout
21 | uses: actions/checkout@v3
22 |
23 | - name: Base Setup
24 | uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1
25 |
26 | - name: Install JupyterLab
27 | run: python -m pip install -U "jupyterlab>=4.0.0,<5"
28 |
29 | - name: Lint the extension
30 | run: |
31 | set -eux
32 | jlpm
33 | jlpm run lint:check
34 |
35 | - name: Build the extension
36 | run: |
37 | set -eux
38 | python -m pip install .
39 |
40 | jupyter labextension list
41 | jupyter labextension list 2>&1 | grep -ie "@jupyterlab/github.*OK"
42 |
43 | python -m jupyterlab.browser_check
44 |
--------------------------------------------------------------------------------
/.github/workflows/publish-changelog.yml:
--------------------------------------------------------------------------------
1 | name: "Publish Changelog"
2 | on:
3 | release:
4 | types: [published]
5 |
6 | workflow_dispatch:
7 | inputs:
8 | branch:
9 | description: "The branch to target"
10 | required: false
11 |
12 | jobs:
13 | publish_changelog:
14 | runs-on: ubuntu-latest
15 | environment: release
16 | steps:
17 | - uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1
18 |
19 | - uses: actions/create-github-app-token@v1
20 | id: app-token
21 | with:
22 | app-id: ${{ vars.APP_ID }}
23 | private-key: ${{ secrets.APP_PRIVATE_KEY }}
24 |
25 | - name: Publish changelog
26 | id: publish-changelog
27 | uses: jupyter-server/jupyter_releaser/.github/actions/publish-changelog@v2
28 | with:
29 | token: ${{ steps.app-token.outputs.token }}
30 | branch: ${{ github.event.inputs.branch }}
31 |
32 | - name: "** Next Step **"
33 | run: |
34 | echo "Merge the changelog update PR: ${{ steps.publish-changelog.outputs.pr_url }}"
35 |
--------------------------------------------------------------------------------
/.github/workflows/binder-on-pr.yaml:
--------------------------------------------------------------------------------
1 | name: Binder Badge
2 | on:
3 | pull_request_target:
4 | types: [opened]
5 |
6 | permissions:
7 | pull-requests: write
8 |
9 | jobs:
10 | binder:
11 | runs-on: ubuntu-latest
12 | steps:
13 | - name: comment on PR with Binder link
14 | uses: actions/github-script@v3
15 | with:
16 | github-token: ${{secrets.GITHUB_TOKEN}}
17 | script: |
18 | var PR_HEAD_USERREPO = process.env.PR_HEAD_USERREPO;
19 | var PR_HEAD_REF = process.env.PR_HEAD_REF;
20 | github.issues.createComment({
21 | issue_number: context.issue.number,
22 | owner: context.repo.owner,
23 | repo: context.repo.repo,
24 | body: `[](https://mybinder.org/v2/gh/${PR_HEAD_USERREPO}/${PR_HEAD_REF}?urlpath=lab) :point_left: Launch a binder notebook on branch _${PR_HEAD_USERREPO}/${PR_HEAD_REF}_`
25 | })
26 | env:
27 | PR_HEAD_REF: ${{ github.event.pull_request.head.ref }}
28 | PR_HEAD_USERREPO: ${{ github.event.pull_request.head.repo.full_name }}
29 |
--------------------------------------------------------------------------------
/binder/postBuild:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | """ perform a development install of jupyterlab-github
3 | On Binder, this will run _after_ the environment has been fully created from
4 | the environment.yml in this directory.
5 | This script should also run locally on Linux/MacOS/Windows:
6 | python3 binder/postBuild
7 | """
8 | import subprocess
9 | import sys
10 | from pathlib import Path
11 |
12 |
13 | ROOT = Path.cwd()
14 |
15 | def _(*args, **kwargs):
16 | """ Run a command, echoing the args
17 | fails hard if something goes wrong
18 | """
19 | print("\n\t", " ".join(args), "\n")
20 | return_code = subprocess.call(args, **kwargs)
21 | if return_code != 0:
22 | print("\nERROR", return_code, " ".join(args))
23 | sys.exit(return_code)
24 |
25 | # verify the environment is self-consistent before even starting
26 | _(sys.executable, "-m", "pip", "check")
27 |
28 | # install the labextension
29 | _(sys.executable, "-m", "pip", "install", "-e", ".")
30 | _(sys.executable, "-m", "jupyter", "labextension", "develop", "--overwrite", ".")
31 |
32 | # verify the environment the extension didn't break anything
33 | _(sys.executable, "-m", "pip", "check")
34 |
35 | # initially list installed extensions to determine if there are any surprises
36 | _("jupyter", "labextension", "list")
37 |
38 |
39 | print("JupyterLab with jupyterlab-github is ready to run with:\n")
40 | print("\tjupyter lab\n")
41 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2017, Project Jupyter Contributors
2 | All rights reserved.
3 |
4 | Redistribution and use in source and binary forms, with or without
5 | modification, are permitted provided that the following conditions are met:
6 |
7 | 1. Redistributions of source code must retain the above copyright notice, this
8 | list of conditions and the following disclaimer.
9 |
10 | 2. Redistributions in binary form must reproduce the above copyright notice,
11 | this list of conditions and the following disclaimer in the documentation
12 | and/or other materials provided with the distribution.
13 |
14 | 3. Neither the name of the copyright holder nor the names of its
15 | contributors may be used to endorse or promote products derived from
16 | this software without specific prior written permission.
17 |
18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
19 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
20 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
22 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
23 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
24 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
25 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
26 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28 |
--------------------------------------------------------------------------------
/.github/workflows/prep-release.yml:
--------------------------------------------------------------------------------
1 | name: "Step 1: Prep Release"
2 | on:
3 | workflow_dispatch:
4 | inputs:
5 | version_spec:
6 | description: "New Version Specifier"
7 | default: "next"
8 | required: false
9 | branch:
10 | description: "The branch to target"
11 | required: false
12 | post_version_spec:
13 | description: "Post Version Specifier"
14 | required: false
15 | silent:
16 | description: "Set a placeholder in the changelog and don't publish the release."
17 | required: false
18 | type: boolean
19 | since:
20 | description: "Use PRs with activity since this date or git reference"
21 | required: false
22 | since_last_stable:
23 | description: "Use PRs with activity since the last stable git tag"
24 | required: false
25 | type: boolean
26 | jobs:
27 | prep_release:
28 | runs-on: ubuntu-latest
29 | permissions:
30 | contents: write
31 | steps:
32 | - uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1
33 |
34 | - name: Prep Release
35 | id: prep-release
36 | uses: jupyter-server/jupyter_releaser/.github/actions/prep-release@v2
37 | with:
38 | token: ${{ secrets.GITHUB_TOKEN }}
39 | version_spec: ${{ github.event.inputs.version_spec }}
40 | silent: ${{ github.event.inputs.silent }}
41 | post_version_spec: ${{ github.event.inputs.post_version_spec }}
42 | target: ${{ github.event.inputs.target }}
43 | branch: ${{ github.event.inputs.branch }}
44 | since: ${{ github.event.inputs.since }}
45 | since_last_stable: ${{ github.event.inputs.since_last_stable }}
46 |
47 | - name: "** Next Step **"
48 | run: |
49 | echo "Optional): Review Draft Release: ${{ steps.prep-release.outputs.release_url }}"
50 |
--------------------------------------------------------------------------------
/.github/workflows/publish-release.yml:
--------------------------------------------------------------------------------
1 | name: "Step 2: Publish Release"
2 | on:
3 | workflow_dispatch:
4 | inputs:
5 | branch:
6 | description: "The target branch"
7 | required: false
8 | release_url:
9 | description: "The URL of the draft GitHub release"
10 | required: false
11 | steps_to_skip:
12 | description: "Comma separated list of steps to skip"
13 | required: false
14 |
15 | jobs:
16 | publish_release:
17 | runs-on: ubuntu-latest
18 | environment: release
19 | permissions:
20 | id-token: write
21 | steps:
22 | - uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1
23 |
24 | - uses: actions/create-github-app-token@v1
25 | id: app-token
26 | with:
27 | app-id: ${{ vars.APP_ID }}
28 | private-key: ${{ secrets.APP_PRIVATE_KEY }}
29 |
30 | - name: Populate Release
31 | id: populate-release
32 | uses: jupyter-server/jupyter_releaser/.github/actions/populate-release@v2
33 | with:
34 | token: ${{ steps.app-token.outputs.token }}
35 | branch: ${{ github.event.inputs.branch }}
36 | release_url: ${{ github.event.inputs.release_url }}
37 | steps_to_skip: ${{ github.event.inputs.steps_to_skip }}
38 |
39 | - name: Finalize Release
40 | id: finalize-release
41 | uses: jupyter-server/jupyter_releaser/.github/actions/finalize-release@v2
42 | with:
43 | token: ${{ steps.app-token.outputs.token }}
44 | release_url: ${{ steps.populate-release.outputs.release_url }}
45 |
46 | - name: "** Next Step **"
47 | if: ${{ success() }}
48 | run: |
49 | echo "Verify the final release"
50 | echo ${{ steps.finalize-release.outputs.release_url }}
51 |
52 | - name: "** Failure Message **"
53 | if: ${{ failure() }}
54 | run: |
55 | echo "Failed to Publish the Draft Release Url:"
56 | echo ${{ steps.populate-release.outputs.release_url }}
57 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.bundle.*
2 | dist/
3 | *.egg-info/
4 | __pycache__/
5 | lib/
6 | test/build/*
7 | node_modules/
8 | .pytest_cache/
9 | .vscode/
10 | .idea/
11 | .ipynb_checkpoints/
12 | npm-debug.log
13 | package-lock.json
14 | .yarn
15 | *.tsbuildinfo
16 | *.stylelintcache
17 | jupyterlab_github/labextension
18 |
19 | # Version file handled by hatch
20 | jupyterlab_github/_version.py
21 |
22 | # Created by https://www.gitignore.io/api/python
23 | # Edit at https://www.gitignore.io/?templates=python
24 |
25 | ### Python ###
26 | # Byte-compiled / optimized / DLL files
27 | *.py[cod]
28 | *$py.class
29 |
30 | # C extensions
31 | *.so
32 |
33 | # Distribution / packaging
34 | .Python
35 | build/
36 | develop-eggs/
37 | dist/
38 | downloads/
39 | eggs/
40 | .eggs/
41 | lib64/
42 | parts/
43 | sdist/
44 | var/
45 | wheels/
46 | pip-wheel-metadata/
47 | share/python-wheels/
48 | .installed.cfg
49 | *.egg
50 | MANIFEST
51 |
52 | # PyInstaller
53 | # Usually these files are written by a python script from a template
54 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
55 | *.manifest
56 | *.spec
57 |
58 | # Installer logs
59 | pip-log.txt
60 | pip-delete-this-directory.txt
61 |
62 | # Unit test / coverage reports
63 | htmlcov/
64 | .tox/
65 | .nox/
66 | .coverage
67 | .coverage.*
68 | .cache
69 | nosetests.xml
70 | coverage.xml
71 | *.cover
72 | .hypothesis/
73 | .pytest_cache/
74 |
75 | # Translations
76 | *.mo
77 | *.pot
78 |
79 | # Scrapy stuff:
80 | .scrapy
81 |
82 | # Sphinx documentation
83 | docs/_build/
84 |
85 | # PyBuilder
86 | target/
87 |
88 | # pyenv
89 | .python-version
90 |
91 | # celery beat schedule file
92 | celerybeat-schedule
93 |
94 | # SageMath parsed files
95 | *.sage.py
96 |
97 | # Spyder project settings
98 | .spyderproject
99 | .spyproject
100 |
101 | # Rope project settings
102 | .ropeproject
103 |
104 | # Mr Developer
105 | .mr.developer.cfg
106 | .project
107 | .pydevproject
108 |
109 | # mkdocs documentation
110 | /site
111 |
112 | # mypy
113 | .mypy_cache/
114 | .dmypy.json
115 | dmypy.json
116 |
117 | # Pyre type checker
118 | .pyre/
119 |
120 | # End of https://www.gitignore.io/api/python
121 |
122 | # OSX files
123 | .DS_Store
124 |
125 | # envrc
126 | .envrc
127 |
128 | # direnv
129 | .direnv
130 |
--------------------------------------------------------------------------------
/style/octocat-dark.svg:
--------------------------------------------------------------------------------
1 |
2 |
58 |
--------------------------------------------------------------------------------
/.github/workflows/packaging.yml:
--------------------------------------------------------------------------------
1 | name: Packaging
2 |
3 | on:
4 | push:
5 | branches: [ main ]
6 | pull_request:
7 | branches: '*'
8 |
9 | env:
10 | PIP_DISABLE_PIP_VERSION_CHECK: 1
11 |
12 | defaults:
13 | run:
14 | shell: bash -l {0}
15 |
16 | jobs:
17 | build:
18 | runs-on: ubuntu-latest
19 |
20 | steps:
21 | - name: Checkout
22 | uses: actions/checkout@v3
23 |
24 | - name: Base Setup
25 | uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1
26 |
27 | - name: Install dependencies
28 | run: python -m pip install -U "jupyterlab>=4.0.0,<5"
29 |
30 | - name: Package the extension
31 | run: |
32 | set -eux
33 | pip install build
34 | python -m build
35 | pip uninstall -y "jupyterlab_github" jupyterlab
36 |
37 | - name: Upload extension packages
38 | uses: actions/upload-artifact@v3
39 | with:
40 | name: extension-artifacts
41 | path: ./dist/jupyterlab_github*
42 | if-no-files-found: error
43 |
44 | install:
45 | runs-on: ${{ matrix.os }}-latest
46 | needs: [build]
47 | strategy:
48 | fail-fast: false
49 | matrix:
50 | os: [ubuntu, macos, windows]
51 | python: ['3.8', '3.11']
52 | include:
53 | - python: '3.8'
54 | dist: 'jupyterlab_github*.tar.gz'
55 | - python: '3.11'
56 | dist: 'jupyterlab_github*.whl'
57 | - os: windows
58 | py_cmd: python
59 | - os: macos
60 | py_cmd: python3
61 | - os: ubuntu
62 | py_cmd: python
63 |
64 | steps:
65 | - name: Install Python
66 | uses: actions/setup-python@v4
67 | with:
68 | python-version: ${{ matrix.python }}
69 | architecture: 'x64'
70 |
71 | - uses: actions/download-artifact@v3
72 | with:
73 | name: extension-artifacts
74 | path: ./dist
75 |
76 | - name: Install prerequisites
77 | run: |
78 | ${{ matrix.py_cmd }} -m pip install pip wheel
79 |
80 | - name: Install package
81 | run: |
82 | cd dist
83 | ${{ matrix.py_cmd }} -m pip install -vv ${{ matrix.dist }}
84 |
85 | - name: Validate environment
86 | run: |
87 | ${{ matrix.py_cmd }} -m pip freeze
88 | ${{ matrix.py_cmd }} -m pip check
89 |
90 | - name: Validate install
91 | run: |
92 | jupyter labextension list
93 | jupyter labextension list 2>&1 | grep -ie "@jupyterlab/github.*OK"
94 |
--------------------------------------------------------------------------------
/style/binder.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | [build-system]
2 | requires = ["hatchling>=1.5.0", "jupyterlab>=4.0.0,<5", "hatch-nodejs-version"]
3 | build-backend = "hatchling.build"
4 |
5 | [project]
6 | name = "jupyterlab_github"
7 | readme = "README.md"
8 | license = { file = "LICENSE" }
9 | requires-python = ">=3.8"
10 | classifiers = [
11 | "Framework :: Jupyter",
12 | "Framework :: Jupyter :: JupyterLab",
13 | "Framework :: Jupyter :: JupyterLab :: 4",
14 | "Framework :: Jupyter :: JupyterLab :: Extensions",
15 | "Framework :: Jupyter :: JupyterLab :: Extensions :: Prebuilt",
16 | "License :: OSI Approved :: BSD License",
17 | "Programming Language :: Python",
18 | "Programming Language :: Python :: 3",
19 | "Programming Language :: Python :: 3.8",
20 | "Programming Language :: Python :: 3.9",
21 | "Programming Language :: Python :: 3.10",
22 | "Programming Language :: Python :: 3.11",
23 | ]
24 | dependencies = [
25 | "jupyterlab>=4.0.0,<5",
26 | ]
27 | dynamic = ["version", "description", "authors", "urls", "keywords"]
28 |
29 | [tool.hatch.version]
30 | source = "nodejs"
31 |
32 | [tool.hatch.metadata.hooks.nodejs]
33 | fields = ["description", "authors", "urls"]
34 |
35 | [tool.hatch.build.targets.sdist]
36 | artifacts = ["jupyterlab_github/labextension"]
37 | exclude = [".github", "binder"]
38 |
39 | [tool.hatch.build.targets.wheel.shared-data]
40 | "jupyter-config/jupyter_server_config.d" = "etc/jupyter/jupyter_server_config.d"
41 | "jupyterlab_github/labextension" = "share/jupyter/labextensions/@jupyterlab/github"
42 | "install.json" = "share/jupyter/labextensions/@jupyterlab/github/install.json"
43 |
44 | [tool.hatch.build.hooks.version]
45 | path = "jupyterlab_github/_version.py"
46 |
47 | [tool.hatch.build.hooks.jupyter-builder]
48 | dependencies = ["hatch-jupyter-builder>=0.5"]
49 | build-function = "hatch_jupyter_builder.npm_builder"
50 | ensured-targets = [
51 | "jupyterlab_github/labextension/static/style.js",
52 | "jupyterlab_github/labextension/package.json",
53 | ]
54 | skip-if-exists = ["jupyterlab_github/labextension/static/style.js"]
55 | optional-editable-build = true
56 |
57 | [tool.hatch.build.hooks.jupyter-builder.build-kwargs]
58 | build_cmd = "build:prod"
59 | npm = ["jlpm"]
60 |
61 | [tool.hatch.build.hooks.jupyter-builder.editable-build-kwargs]
62 | build_cmd = "build"
63 | npm = ["jlpm"]
64 | source_dir = "src"
65 | build_dir = "jupyterlab_github/labextension"
66 |
67 | [tool.jupyter-releaser.options]
68 | version_cmd = "hatch version"
69 |
70 | [tool.jupyter-releaser.hooks]
71 | before-build-npm = [
72 | "python -m pip install 'jupyterlab>=4.0.0,<5'",
73 | "jlpm",
74 | "jlpm build:prod"
75 | ]
76 | before-build-python = ["jlpm clean:all"]
77 |
78 | [tool.check-wheel-contents]
79 | ignore = ["W002"]
80 |
--------------------------------------------------------------------------------
/style/base.css:
--------------------------------------------------------------------------------
1 | /* -----------------------------------------------------------------------------
2 | | Copyright (c) Jupyter Development Team.
3 | | Distributed under the terms of the Modified BSD License.
4 | |---------------------------------------------------------------------------- */
5 |
6 | [data-jp-theme-light='true'] .jp-GitHub-icon {
7 | background-image: 'url(octocat-light.svg)';
8 | }
9 |
10 | [data-jp-theme-light='false'] .jp-GitHub-icon {
11 | background-image: 'url(octocat-dark.svg)';
12 | }
13 |
14 | .jp-GitHubBrowser {
15 | background-color: var(--jp-layout-color1);
16 | height: 100%;
17 | }
18 |
19 | .jp-GitHubBrowser .jp-FileBrowser {
20 | flex-grow: 1;
21 | height: 100%;
22 | }
23 |
24 | .jp-GitHubUserInput {
25 | overflow: hidden;
26 | white-space: nowrap;
27 | text-align: center;
28 | font-size: large;
29 | padding: 0;
30 | background-color: var(--jp-layout-color1);
31 | }
32 |
33 | .jp-FileBrowser-toolbar.jp-Toolbar .jp-Toolbar-item.jp-GitHubUserInput {
34 | flex: 8 8;
35 | }
36 |
37 | .jp-GitHubUserInput-wrapper {
38 | background-color: var(--jp-input-active-background);
39 | border: var(--jp-border-width) solid var(--jp-border-color2);
40 | height: 30px;
41 | padding: 0 0 0 12px;
42 | margin: 0 4px 0 0;
43 | }
44 |
45 | .jp-GitHubUserInput-wrapper:focus-within {
46 | border: var(--jp-border-width) solid var(--md-blue-500);
47 | box-shadow: inset 0 0 4px var(--md-blue-300);
48 | }
49 |
50 | .jp-GitHubUserInput-wrapper input {
51 | background: transparent;
52 | float: left;
53 | border: none;
54 | outline: none;
55 | font-size: var(--jp-ui-font-size3);
56 | color: var(--jp-ui-font-color0);
57 | width: calc(100% - 18px);
58 | line-height: 28px;
59 | }
60 |
61 | .jp-GitHubUserInput-wrapper input::placeholder {
62 | color: var(--jp-ui-font-color3);
63 | font-size: var(--jp-ui-font-size1);
64 | text-transform: uppercase;
65 | }
66 |
67 | .jp-GitHubBrowser .jp-ToolbarButton.jp-Toolbar-item.jp-GitHub-toolbar-item {
68 | display: block;
69 | }
70 |
71 | .jp-GitHubBrowser .jp-ToolbarButton.jp-Toolbar-item {
72 | display: none;
73 | }
74 |
75 | .jp-GitHubBrowser .jp-DirListing-headerItem.jp-id-modified {
76 | display: none;
77 | }
78 |
79 | .jp-GitHubBrowser .jp-DirListing-itemModified {
80 | display: none;
81 | }
82 |
83 | .jp-GitHubErrorPanel {
84 | position: absolute;
85 | display: flex;
86 | flex-direction: column;
87 | justify-content: center;
88 | align-items: center;
89 | z-index: 10;
90 | left: 0;
91 | top: 0;
92 | width: 100%;
93 | height: 100%;
94 | background: var(--jp-layout-color2);
95 | }
96 |
97 | .jp-GitHubErrorImage {
98 | background-size: 100%;
99 | width: 200px;
100 | height: 165px;
101 | background-image: 'url(octocat_error.png)';
102 | }
103 |
104 | .jp-GitHubErrorText {
105 | font-size: var(--jp-ui-font-size3);
106 | color: var(--jp-ui-font-color1);
107 | text-align: center;
108 | padding: 12px;
109 | }
110 |
111 | .jp-GitHubBrowser .jp-MyBinderButton {
112 | background-image: 'url(binder.svg)';
113 | }
114 |
115 | .jp-GitHubBrowser .jp-MyBinderButton-disabled {
116 | opacity: 0.3;
117 | }
118 |
119 | #setting-editor .jp-PluginList-icon.jp-GitHub-icon {
120 | background-size: 85%;
121 | background-repeat: no-repeat;
122 | background-position: center;
123 | }
124 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@jupyterlab/github",
3 | "version": "4.0.0",
4 | "description": "JupyterLab viewer for GitHub repositories",
5 | "keywords": [
6 | "github",
7 | "jupyter",
8 | "jupyterlab",
9 | "jupyterlab-extension"
10 | ],
11 | "homepage": "https://github.com/jupyterlab/jupyterlab-github",
12 | "bugs": {
13 | "url": "https://github.com/jupyterlab/jupyterlab-github/issues"
14 | },
15 | "repository": {
16 | "type": "git",
17 | "url": "https://github.com/jupyterlab/jupyterlab-github.git"
18 | },
19 | "license": "BSD-3-Clause",
20 | "author": {
21 | "name": "Ian Rose",
22 | "email": "jupyter@googlegroups.com"
23 | },
24 | "files": [
25 | "lib/*/*d.ts",
26 | "lib/*/*.js",
27 | "lib/*.d.ts",
28 | "lib/*.js",
29 | "schema/*.json",
30 | "style/*.*",
31 | "style/index.js"
32 | ],
33 | "main": "lib/index.js",
34 | "types": "lib/index.d.ts",
35 | "directories": {
36 | "lib": "lib/"
37 | },
38 | "scripts": {
39 | "build": "jlpm run build:lib && jlpm run build:labextension:dev",
40 | "build:labextension": "jupyter labextension build .",
41 | "build:labextension:dev": "jupyter labextension build --development True .",
42 | "build:lib": "tsc",
43 | "build:prod": "jlpm run build:lib && jlpm run build:labextension",
44 | "build:test": "cd test && ./build-tests.sh",
45 | "clean": "jlpm run clean:lib",
46 | "clean:all": "jlpm run clean:lib && jlpm run clean:labextension",
47 | "clean:labextension": "rimraf jupyterlab_github/labextension",
48 | "clean:lib": "rimraf lib tsconfig.tsbuildinfo",
49 | "lint": "jlpm && jlpm prettier && jlpm eslint && jlpm stylelint",
50 | "lint:check": "jlpm prettier:check && jlpm eslint:check && jlpm stylelint:check",
51 | "eslint": "eslint . --ext .ts,.tsx --fix",
52 | "eslint:check": "eslint . --ext .ts,.tsx",
53 | "prettier": "prettier --write \"**/*{.ts,.tsx,.js,.jsx,.css,.json,.md}\"",
54 | "prettier:check": "prettier --list-different \"**/*{.ts,.tsx,.js,.jsx,.css,.json,.md}\"",
55 | "stylelint": "jlpm stylelint:check --fix",
56 | "stylelint:check": "stylelint --cache \"style/*.css\"",
57 | "stylelint:files": "stylelint --fix",
58 | "install:extension": "jupyter labextension develop --overwrite .",
59 | "precommit": "lint-staged",
60 | "test": "cd test && ./run-tests.sh",
61 | "watch": "run-p watch:src watch:labextension",
62 | "watch:labextension": "jupyter labextension watch .",
63 | "watch:src": "tsc -w"
64 | },
65 | "lint-staged": {
66 | "**/*{.ts,.tsx,.css,.json,.md}": [
67 | "prettier --write",
68 | "git add"
69 | ]
70 | },
71 | "dependencies": {
72 | "@jupyterlab/application": "^4.0.0",
73 | "@jupyterlab/apputils": "^4.0.0",
74 | "@jupyterlab/coreutils": "^6.0.0",
75 | "@jupyterlab/docmanager": "^4.0.0",
76 | "@jupyterlab/docregistry": "^4.0.0",
77 | "@jupyterlab/filebrowser": "^4.0.0",
78 | "@jupyterlab/services": "^7.0.0",
79 | "@jupyterlab/settingregistry": "^4.0.0",
80 | "@jupyterlab/ui-components": "^4.0.0",
81 | "@lumino/algorithm": "^2.0.0",
82 | "@lumino/messaging": "^2.0.0",
83 | "@lumino/signaling": "^2.0.0",
84 | "@lumino/widgets": "^2.0.0",
85 | "base64-js": "^1.5.0"
86 | },
87 | "devDependencies": {
88 | "@jupyterlab/builder": "^4.0.0",
89 | "@types/base64-js": "^1.3.0",
90 | "@types/text-encoding": "^0.0.35",
91 | "@typescript-eslint/eslint-plugin": "^5.55.0",
92 | "@typescript-eslint/eslint-plugin-tslint": "^6.1.0",
93 | "@typescript-eslint/parser": "^5.55.0",
94 | "eslint": "^8.36.0",
95 | "eslint-config-prettier": "^8.7.0",
96 | "eslint-plugin-import": "^2.27.5",
97 | "eslint-plugin-no-null": "^1.0.2",
98 | "eslint-plugin-prettier": "^4.2.1",
99 | "husky": "^8.0.0",
100 | "lint-staged": "^13.2.0",
101 | "mkdirp": "^1.0.3",
102 | "npm-run-all": "^4.1.5",
103 | "prettier": "^2.8.7",
104 | "rimraf": "^4.4.1",
105 | "stylelint": "^14.9.1",
106 | "stylelint-config-prettier": "^9.0.4",
107 | "stylelint-config-recommended": "^8.0.0",
108 | "stylelint-config-standard": "^26.0.0",
109 | "stylelint-prettier": "^2.0.0",
110 | "typescript": "~5.0.1"
111 | },
112 | "jupyterlab": {
113 | "extension": true,
114 | "discovery": {
115 | "server": {
116 | "managers": [
117 | "pip"
118 | ],
119 | "base": {
120 | "name": "jupyterlab_github"
121 | }
122 | }
123 | },
124 | "schemaDir": "schema",
125 | "outputDir": "jupyterlab_github/labextension"
126 | },
127 | "sideEffects": [
128 | "style/*.css",
129 | "style/index.js"
130 | ],
131 | "styleModule": "style/index.js"
132 | }
133 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | // Copyright (c) Jupyter Development Team.
2 | // Distributed under the terms of the Modified BSD License.
3 |
4 | import {
5 | ILayoutRestorer,
6 | JupyterFrontEnd,
7 | JupyterFrontEndPlugin
8 | } from '@jupyterlab/application';
9 |
10 | import { Dialog, showDialog } from '@jupyterlab/apputils';
11 |
12 | import { LabIcon } from '@jupyterlab/ui-components';
13 |
14 | import { IDocumentManager } from '@jupyterlab/docmanager';
15 |
16 | import { IFileBrowserFactory } from '@jupyterlab/filebrowser';
17 |
18 | import { ISettingRegistry } from '@jupyterlab/settingregistry';
19 |
20 | import { GitHubDrive, DEFAULT_GITHUB_BASE_URL } from './contents';
21 |
22 | import { GitHubFileBrowser } from './browser';
23 |
24 | import GitHubSvgStr from '../style/octocat-light.svg';
25 |
26 | /**
27 | * GitHub filebrowser plugin state namespace.
28 | */
29 | const NAMESPACE = 'github-filebrowser';
30 |
31 | /**
32 | * The ID for the plugin.
33 | */
34 | const PLUGIN_ID = '@jupyterlab/github:drive';
35 |
36 | /**
37 | * GitHub Icon class.
38 | */
39 | export const gitHubIcon = new LabIcon({
40 | name: `${NAMESPACE}:icon`,
41 | svgstr: GitHubSvgStr
42 | });
43 |
44 | /**
45 | * The JupyterLab plugin for the GitHub Filebrowser.
46 | */
47 | const fileBrowserPlugin: JupyterFrontEndPlugin = {
48 | id: PLUGIN_ID,
49 | requires: [IDocumentManager, IFileBrowserFactory, ISettingRegistry],
50 | optional: [ILayoutRestorer],
51 | activate: activateFileBrowser,
52 | autoStart: true
53 | };
54 |
55 | /**
56 | * Activate the file browser.
57 | */
58 | function activateFileBrowser(
59 | app: JupyterFrontEnd,
60 | manager: IDocumentManager,
61 | factory: IFileBrowserFactory,
62 | settingRegistry: ISettingRegistry,
63 | restorer: ILayoutRestorer | null
64 | ): void {
65 | // Add the GitHub backend to the contents manager.
66 | const drive = new GitHubDrive(app.docRegistry);
67 | manager.services.contents.addDrive(drive);
68 |
69 | // Create the embedded filebrowser. GitHub repos likely
70 | // don't need as often of a refresh interval as normal ones,
71 | // and rate-limiting can be an issue, so we give a 5 minute
72 | // refresh interval.
73 | const browser = factory.createFileBrowser(NAMESPACE, {
74 | driveName: drive.name,
75 | refreshInterval: 300000
76 | });
77 |
78 | const gitHubBrowser = new GitHubFileBrowser(browser, drive);
79 |
80 | gitHubBrowser.title.icon = gitHubIcon;
81 | gitHubBrowser.title.iconClass = 'jp-SideBar-tabIcon';
82 | gitHubBrowser.title.caption = 'Browse GitHub';
83 |
84 | gitHubBrowser.id = 'github-file-browser';
85 |
86 | // Add the file browser widget to the application restorer.
87 | if (restorer) {
88 | restorer.add(gitHubBrowser, NAMESPACE);
89 | }
90 | app.shell.add(gitHubBrowser, 'left', { rank: 102 });
91 |
92 | let shouldWarn = false;
93 | const onSettingsUpdated = async (settings: ISettingRegistry.ISettings) => {
94 | const baseUrl = settings.get('baseUrl').composite as
95 | | string
96 | | null
97 | | undefined;
98 | const accessToken = settings.get('accessToken').composite as
99 | | string
100 | | null
101 | | undefined;
102 | drive.baseUrl = baseUrl || DEFAULT_GITHUB_BASE_URL;
103 | if (accessToken) {
104 | let proceed = true;
105 | if (shouldWarn) {
106 | proceed = await Private.showWarning();
107 | }
108 | if (!proceed) {
109 | settings.remove('accessToken');
110 | } else {
111 | drive.accessToken = accessToken;
112 | }
113 | } else {
114 | drive.accessToken = null;
115 | }
116 | };
117 |
118 | // Fetch the initial state of the settings.
119 | Promise.all([settingRegistry.load(PLUGIN_ID), app.restored])
120 | .then(([settings]) => {
121 | settings.changed.connect(onSettingsUpdated);
122 | onSettingsUpdated(settings);
123 | // Don't warn about access token on initial page load, but do for every setting thereafter.
124 | shouldWarn = true;
125 | const defaultRepo = settings.get('defaultRepo').composite as
126 | | string
127 | | null;
128 | if (defaultRepo) {
129 | browser.model.restored.then(() => {
130 | browser.model.cd(`/${defaultRepo}`);
131 | });
132 | }
133 | })
134 | .catch((reason: Error) => {
135 | console.error(reason.message);
136 | });
137 |
138 | return;
139 | }
140 |
141 | export default fileBrowserPlugin;
142 |
143 | /**
144 | * A namespace for module-private functions.
145 | */
146 | namespace Private {
147 | /**
148 | * Show a warning dialog about security.
149 | *
150 | * @returns whether the user accepted the dialog.
151 | */
152 | export async function showWarning(): Promise {
153 | return showDialog({
154 | title: 'Security Alert!',
155 | body:
156 | 'Adding a client side access token can pose a security risk! ' +
157 | 'Please consider using the server extension instead.' +
158 | 'Do you want to continue?',
159 | buttons: [
160 | Dialog.cancelButton({ label: 'CANCEL' }),
161 | Dialog.warnButton({ label: 'PROCEED' })
162 | ]
163 | }).then(result => {
164 | if (result.button.accept) {
165 | return true;
166 | } else {
167 | return false;
168 | }
169 | });
170 | }
171 | }
172 |
--------------------------------------------------------------------------------
/jupyterlab_github/__init__.py:
--------------------------------------------------------------------------------
1 | import re, json, copy
2 |
3 | from tornado import web
4 | from tornado.httputil import url_concat
5 | from tornado.httpclient import AsyncHTTPClient, HTTPRequest, HTTPError
6 |
7 | from traitlets import Unicode, Bool
8 | from traitlets.config import Configurable
9 |
10 | from jupyter_server.utils import url_path_join, url_escape
11 | from jupyter_server.base.handlers import APIHandler
12 |
13 | from ._version import __version__
14 |
15 |
16 | link_regex = re.compile(r'<([^>]*)>;\s*rel="([\w]*)\"')
17 |
18 |
19 | class GitHubConfig(Configurable):
20 | """
21 | Allows configuration of access to the GitHub api
22 | """
23 | allow_client_side_access_token = Bool(
24 | False,
25 | help=(
26 | "If True the access token specified in the JupyterLab settings "
27 | "will take precedence. If False the token specified in JupyterLab "
28 | "will be ignored. Storing your access token in the client can "
29 | "present a security risk so be careful if enabling this setting."
30 | )
31 | ).tag(config=True)
32 |
33 | api_url = Unicode(
34 | 'https://api.github.com',
35 | help="The url for the GitHub api"
36 | ).tag(config=True)
37 |
38 | access_token = Unicode(
39 | '',
40 | help="A personal access token for GitHub."
41 | ).tag(config=True)
42 |
43 | validate_cert = Bool(
44 | True,
45 | help=(
46 | "Whether to validate the servers' SSL certificate on requests "
47 | "made to the GitHub api. In general this is a bad idea so only "
48 | "disable SSL validation if you know what you are doing!"
49 | )
50 | ).tag(config=True)
51 |
52 |
53 | class GitHubHandler(APIHandler):
54 | """
55 | A proxy for the GitHub API v3.
56 |
57 | The purpose of this proxy is to provide authentication to the API requests
58 | which allows for a higher rate limit. Without this, the rate limit on
59 | unauthenticated calls is so limited as to be practically useless.
60 | """
61 |
62 | client = AsyncHTTPClient()
63 |
64 | @web.authenticated
65 | async def get(self, path):
66 | """
67 | Proxy API requests to GitHub, adding authentication parameter(s) if
68 | they have been set.
69 | """
70 |
71 | # Get access to the notebook config object
72 | c = GitHubConfig(config=self.config)
73 | try:
74 | query = self.request.query_arguments
75 | params = {key: query[key][0].decode() for key in query}
76 | api_path = url_path_join(c.api_url, url_escape(path))
77 | params['per_page'] = 100
78 |
79 | access_token = params.pop('access_token', None)
80 | if access_token and c.allow_client_side_access_token == True:
81 | token = access_token
82 | elif access_token and c.allow_client_side_access_token == False:
83 | msg = (
84 | "Client side (JupyterLab) access tokens have been "
85 | "disabled for security reasons.\nPlease remove your "
86 | "access token from JupyterLab and instead add it to "
87 | "your notebook configuration file:\n"
88 | "c.GitHubConfig.access_token = ''\n"
89 | )
90 | raise HTTPError(403, msg)
91 | elif c.access_token != '':
92 | # Preferentially use the config access_token if set
93 | token = c.access_token
94 | else:
95 | token = ''
96 |
97 | api_path = url_concat(api_path, params)
98 |
99 | request = HTTPRequest(
100 | api_path,
101 | validate_cert=c.validate_cert,
102 | user_agent='JupyterLab GitHub',
103 | headers={"Authorization": "token {}".format(token)}
104 | )
105 | response = await self.client.fetch(request)
106 | data = json.loads(response.body.decode('utf-8'))
107 |
108 | # Check if we need to paginate results.
109 | # If so, get pages until all the results
110 | # are loaded into the data buffer.
111 | next_page_path = self._maybe_get_next_page_path(response)
112 | while next_page_path:
113 | request = copy.copy(request)
114 | request.url = next_page_path
115 | response = await self.client.fetch(request)
116 | next_page_path = self._maybe_get_next_page_path(response)
117 | data.extend(json.loads(response.body.decode('utf-8')))
118 |
119 | # Send the results back.
120 | self.finish(json.dumps(data))
121 |
122 | except HTTPError as err:
123 | self.set_status(err.code)
124 | message = err.response.body if err.response else str(err.code)
125 | self.finish(message)
126 |
127 | def _maybe_get_next_page_path(self, response):
128 | # If there is a 'Link' header in the response, we
129 | # need to paginate.
130 | link_headers = response.headers.get_list('Link')
131 | next_page_path = None
132 | if link_headers:
133 | links = {}
134 | matched = link_regex.findall(link_headers[0])
135 | for match in matched:
136 | links[match[1]] = match[0]
137 | next_page_path = links.get('next', None)
138 |
139 | return next_page_path
140 |
141 |
142 | def _jupyter_labextension_paths():
143 | return [
144 | {
145 | "src": "labextension",
146 | "dest": "@jupyterlab/github",
147 | }
148 | ]
149 |
150 |
151 | def _jupyter_server_extension_paths():
152 | return [{
153 | 'module': 'jupyterlab_github'
154 | }]
155 |
156 |
157 | def load_jupyter_server_extension(nb_server_app):
158 | """
159 | Called when the extension is loaded.
160 |
161 | Args:
162 | nb_server_app (NotebookWebApplication): handle to the Notebook webserver instance.
163 | """
164 | web_app = nb_server_app.web_app
165 | base_url = web_app.settings['base_url']
166 | endpoint = url_path_join(base_url, 'github')
167 | handlers = [(endpoint + "(.*)", GitHubHandler)]
168 | web_app.add_handlers('.*$', handlers)
169 |
--------------------------------------------------------------------------------
/src/github.ts:
--------------------------------------------------------------------------------
1 | // Copyright (c) Jupyter Development Team.
2 | // Distributed under the terms of the Modified BSD License.
3 |
4 | import { ServerConnection } from '@jupyterlab/services';
5 |
6 | /**
7 | * Make a client-side request to the GitHub API.
8 | *
9 | * @param url - the api path for the GitHub API v3
10 | * (not including the base url).
11 | *
12 | * @returns a Promise resolved with the JSON response.
13 | */
14 | export function browserApiRequest(url: string): Promise {
15 | return window.fetch(url).then(response => {
16 | if (response.status !== 200) {
17 | return response.json().then(data => {
18 | throw new ServerConnection.ResponseError(response, data.message);
19 | });
20 | }
21 | return response.json();
22 | });
23 | }
24 |
25 | /**
26 | * Make a request to the notebook server proxy for the
27 | * GitHub API.
28 | *
29 | * @param url - the api path for the GitHub API v3
30 | * (not including the base url)
31 | *
32 | * @param settings - the settings for the current notebook server.
33 | *
34 | * @returns a Promise resolved with the JSON response.
35 | */
36 | export function proxiedApiRequest(
37 | url: string,
38 | settings: ServerConnection.ISettings
39 | ): Promise {
40 | return ServerConnection.makeRequest(url, {}, settings).then(response => {
41 | if (response.status !== 200) {
42 | return response.json().then(data => {
43 | throw new ServerConnection.ResponseError(response, data.message);
44 | });
45 | }
46 | return response.json();
47 | });
48 | }
49 |
50 | /**
51 | * Typings representing contents from the GitHub API v3.
52 | * Cf: https://developer.github.com/v3/repos/contents/
53 | */
54 | export interface GitHubContents {
55 | /**
56 | * The type of the file.
57 | */
58 | type: 'file' | 'dir' | 'submodule' | 'symlink';
59 |
60 | /**
61 | * The size of the file (in bytes).
62 | */
63 | size: number;
64 |
65 | /**
66 | * The name of the file.
67 | */
68 | name: string;
69 |
70 | /**
71 | * The path of the file in the repository.
72 | */
73 | path: string;
74 |
75 | /**
76 | * A unique sha identifier for the file.
77 | */
78 | sha: string;
79 |
80 | /**
81 | * The URL for the file in the GitHub API.
82 | */
83 | url: string;
84 |
85 | /**
86 | * The URL for git access to the file.
87 | */
88 | // tslint:disable-next-line
89 | git_url: string;
90 |
91 | /**
92 | * The URL for the file in the GitHub UI.
93 | */
94 | // tslint:disable-next-line
95 | html_url: string;
96 |
97 | /**
98 | * The raw download URL for the file.
99 | */
100 | // tslint:disable-next-line
101 | download_url: string;
102 |
103 | /**
104 | * Unsure the purpose of these.
105 | */
106 | _links: {
107 | git: string;
108 |
109 | self: string;
110 |
111 | html: string;
112 | };
113 | }
114 |
115 | /**
116 | * Typings representing file contents from the GitHub API v3.
117 | * Cf: https://developer.github.com/v3/repos/contents/#response-if-content-is-a-file
118 | */
119 | export interface GitHubFileContents extends GitHubContents {
120 | /**
121 | * The type of the contents.
122 | */
123 | type: 'file';
124 |
125 | /**
126 | * Encoding of the content. All files are base64 encoded.
127 | */
128 | encoding: 'base64';
129 |
130 | /**
131 | * The actual base64 encoded contents.
132 | */
133 | content?: string;
134 | }
135 |
136 | /**
137 | * Typings representing a directory from the GitHub API v3.
138 | */
139 | export interface GitHubDirectoryContents extends GitHubContents {
140 | /**
141 | * The type of the contents.
142 | */
143 | type: 'dir';
144 | }
145 |
146 | /**
147 | * Typings representing a blob from the GitHub API v3.
148 | * Cf: https://developer.github.com/v3/git/blobs/#response
149 | */
150 | export interface GitHubBlob {
151 | /**
152 | * The base64-encoded contents of the file.
153 | */
154 | content: string;
155 |
156 | /**
157 | * The encoding of the contents. Always base64.
158 | */
159 | encoding: 'base64';
160 |
161 | /**
162 | * The URL for the blob.
163 | */
164 | url: string;
165 |
166 | /**
167 | * The unique sha for the blob.
168 | */
169 | sha: string;
170 |
171 | /**
172 | * The size of the blob, in bytes.
173 | */
174 | size: number;
175 | }
176 |
177 | /**
178 | * Typings representing symlink contents from the GitHub API v3.
179 | * Cf: https://developer.github.com/v3/repos/contents/#response-if-content-is-a-symlink
180 | */
181 | export interface GitHubSymlinkContents extends GitHubContents {
182 | /**
183 | * The type of the contents.
184 | */
185 | type: 'symlink';
186 | }
187 |
188 | /**
189 | * Typings representing submodule contents from the GitHub API v3.
190 | * Cf: https://developer.github.com/v3/repos/contents/#response-if-content-is-a-submodule
191 | */
192 | export interface GitHubSubmoduleContents extends GitHubContents {
193 | /**
194 | * The type of the contents.
195 | */
196 | type: 'submodule';
197 | }
198 |
199 | /**
200 | * Typings representing directory contents from the GitHub API v3.
201 | * Cf: https://developer.github.com/v3/repos/contents/#response-if-content-is-a-directory
202 | */
203 | export type GitHubDirectoryListing = GitHubContents[];
204 |
205 | /**
206 | * Typings representing repositories from the GitHub API v3.
207 | * Cf: https://developer.github.com/v3/repos/#list-organization-repositories
208 | *
209 | * #### Notes
210 | * This is incomplete.
211 | */
212 | export interface GitHubRepo {
213 | /**
214 | * ID for the repository.
215 | */
216 | id: number;
217 |
218 | /**
219 | * The owner of the repository.
220 | */
221 | owner: any;
222 |
223 | /**
224 | * The name of the repository.
225 | */
226 | name: string;
227 |
228 | /**
229 | * The full name of the repository, including the owner name.
230 | */
231 | // tslint:disable-next-line
232 | full_name: string;
233 |
234 | /**
235 | * A description of the repository.
236 | */
237 | description: string;
238 |
239 | /**
240 | * Whether the repository is private.
241 | */
242 | private: boolean;
243 |
244 | /**
245 | * Whether the repository is a fork.
246 | */
247 | fork: boolean;
248 |
249 | /**
250 | * The URL for the repository in the GitHub API.
251 | */
252 | url: string;
253 |
254 | /**
255 | * The URL for the repository in the GitHub UI.
256 | */
257 | // tslint:disable-next-line
258 | html_url: string;
259 | }
260 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # JupyterLab GitHub
2 |
3 | [](https://mybinder.org/v2/gh/jupyterlab/jupyterlab-github/main?urlpath=lab)
4 |
5 | A JupyterLab extension for accessing GitHub repositories.
6 |
7 | ### What this extension is
8 |
9 | When you install this extension, an additional filebrowser tab will be added
10 | to the left area of JupyterLab. This filebrowser allows you to select GitHub
11 | organizations and users, browse their repositories, and open the files in those
12 | repositories. If those files are notebooks, you can run them just as you would
13 | any other notebook. You can also attach a kernel to text files and run those.
14 | Basically, you should be able to open any file in a repository that JupyterLab can handle.
15 |
16 | Here is a screenshot of the plugin opening this very file on GitHub:
17 | 
18 |
19 | ### What this extension is not
20 |
21 | This is not an extension that provides full GitHub access, such as
22 | saving files, making commits, forking repositories, etc.
23 | For it to be so, it would need to more-or-less reinvent the GitHub website,
24 | which represents a huge increase in complexity for the extension.
25 |
26 | ### A note on rate-limiting
27 |
28 | This extension has both a client-side component (that is, JavaScript that is bundled
29 | with JupyterLab), and a server-side component (that is, Python code that is added
30 | to the Jupyter server). This extension _will_ work with out the server extension,
31 | with a major caveat: when making unauthenticated requests to GitHub
32 | (as we must do to get repository data), GitHub imposes fairly strict rate-limits
33 | on how many requests we can make. As such, you are likely to hit that limit
34 | within a few minutes of work. You will then have to wait up to an hour to regain access.
35 |
36 | For that reason, we recommend that you take the time and effort to set up the server
37 | extension as well as the lab extension, which will allow you to access higher rate-limits.
38 | This process is described in the [installation](#Installation) section.
39 |
40 | ## Prerequisites
41 |
42 | - JupyterLab > 3.0
43 | - A GitHub account for the server extension
44 |
45 | ## Installation
46 |
47 | As discussed above, this extension has both a server extension and a lab extension.
48 | Both extensions will be installed by default when installing from PyPI, but you may
49 | have only lab extension installed if you used the Extension Manager in JupyterLab.
50 |
51 | We recommend completing the steps described below as to not be rate-limited.
52 | The purpose of the server extension is to add GitHub credentials that you will need to acquire
53 | from https://github.com/settings/developers, and then to proxy your request to GitHub.
54 |
55 | For JupyterLab version older than 3 please see the instructions on the
56 | [2.x branch](https://github.com/jupyterlab/jupyterlab-github/tree/2.x).
57 |
58 | ### 1. Installing both server and prebuilt lab extension
59 |
60 | #### JupyterLab 4.x
61 |
62 | To install the both the server extension and (prebuilt) lab extension, enter the following in your terminal:
63 |
64 | ```bash
65 | pip install jupyterlab-github
66 | ```
67 |
68 | #### JupyterLab 3.x
69 |
70 | We need to pin the extension version to `3.0.1` for making it work on the JupyterLab 3.x.
71 |
72 | ```bash
73 | pip install 'jupyterlab-github==3.0.1'
74 | ```
75 |
76 | After restarting JupyterLab, the extension should work, and you can experience
77 | the joys of being rate-limited first-hand!
78 |
79 | ### 2. Getting your credentials from GitHub
80 |
81 | There are two approaches to getting credentials from GitHub:
82 | (1) you can get an access token, (2) you can register an OAuth app.
83 | The second approach is not recommended, and will be removed in a future release.
84 |
85 | #### Getting an access token (**recommended**)
86 |
87 | You can get an access token by following these steps:
88 |
89 | 1. [Verify](https://help.github.com/articles/verifying-your-email-address) your email address with GitHub.
90 | 1. Go to your account settings on GitHub and select "Developer Settings" from the left panel.
91 | 1. On the left, select "Personal access tokens"
92 | 1. Click the "Generate new token" button, and enter your password.
93 | 1. Give the token a description, and check the "**repo**" scope box.
94 | 1. Click "Generate token"
95 | 1. You should be given a string which will be your access token.
96 |
97 | Remember that this token is effectively a password for your GitHub account.
98 | _Do not_ share it online or check the token into version control,
99 | as people can use it to access all of your data on GitHub.
100 |
101 | #### Setting up an OAuth application (**deprecated**)
102 |
103 | This approach to authenticating with GitHub is deprecated, and will be removed in a future release.
104 | New users should use the access token approach.
105 | You can register an OAuth application with GitHub by following these steps:
106 |
107 | 1. Log into your GitHub account.
108 | 1. Go to https://github.com/settings/developers and select the "OAuth Apps" tab on the left.
109 | 1. Click the "New OAuth App" button.
110 | 1. Fill out a name, homepage URL, description, and callback URL in the form.
111 | This extension does not actually use OAuth, so these values actually _do not matter much_,
112 | you just need to enter them to register the application.
113 | 1. Click the "Register application" button.
114 | 1. You should be taken to a new page with the new application information.
115 | If you see fields showing "Client ID" and "Client Secret", congratulations!
116 | These are the strings we need, and you have successfuly set up the application.
117 |
118 | It is important to note that the "Client Secret" string is, as the name suggests, a secret.
119 | _Do not_ share this value online, as people may be able to use it to impersonate you on GitHub.
120 |
121 | ### 3. Enabling and configuring the server extension
122 |
123 | The server extension will be enabled by default on new JupyterLab installations
124 | if you installed it with pip. If you used Extension Manager in JupyterLab,
125 | please uninstall the extension and install it again with the instructions from point (1).
126 |
127 | Confirm that the server extension is installed and enabled with:
128 |
129 | ```bash
130 | jupyter server extension list
131 | ```
132 |
133 | you should see the following:
134 |
135 | ```
136 | - Validating jupyterlab_github...
137 | jupyterlab_github 4.0.0 OK
138 | ```
139 |
140 | On some older installations (e.g. old JupyterHub versions) which use jupyter
141 | `notebook` server instead of the new `jupyter-server`, the extension needs to
142 | show up on the legacy `serverextensions` list (note: no space between _server_ and _extension_):
143 |
144 | ```bash
145 | jupyter serverextension list
146 | ```
147 |
148 | If the extension is not enabled run:
149 |
150 | ```bash
151 | jupyter server extension enable jupyterlab_github
152 | ```
153 |
154 | or if using the legacy `notebook` server:
155 |
156 | ```bash
157 | jupyter serverextension enable jupyterlab_github
158 | ```
159 |
160 | You now need to add the credentials you got from GitHub
161 | to your server configuration file. Instructions for generating a configuration
162 | file can be found [here](https://jupyter-server.readthedocs.io/en/stable/users/configuration.html#configuring-a-jupyter-server).
163 | Once you have identified this file, add the following lines to it:
164 |
165 | ```python
166 | c.GitHubConfig.access_token = '< YOUR_ACCESS_TOKEN >'
167 | ```
168 |
169 | where "`< YOUR_ACCESS_TOKEN >`" is the string value you obtained above.
170 | If you generated an OAuth app, instead enter the following:
171 |
172 | ```python
173 | c.GitHubConfig.client_id = '< YOUR_CLIENT_ID >'
174 | c.GitHubConfig.client_secret = '< YOUR_CLIENT_SECRET >'
175 | ```
176 |
177 | where "`< YOUR_CLIENT_ID >`" and "`< YOUR_CLIENT_SECRET >`" are the app values you obtained above.
178 |
179 | With this, you should be done! Launch JupyterLab and look for the GitHub tab on the left!
180 |
181 | ## Customization
182 |
183 | You can set the plugin to start showing a particular repository at launch time.
184 | Open the "Advanced Settings" editor in the Settings menu,
185 | and under the GitHub settings add
186 |
187 | ```json
188 | {
189 | "defaultRepo": "owner/repository"
190 | }
191 | ```
192 |
193 | where `owner` is the GitHub user/org,
194 | and `repository` is the name of the repository you want to open.
195 |
--------------------------------------------------------------------------------
/src/browser.ts:
--------------------------------------------------------------------------------
1 | // Copyright (c) Jupyter Development Team.
2 | // Distributed under the terms of the Modified BSD License.
3 |
4 | import { ToolbarButton } from '@jupyterlab/apputils';
5 |
6 | import { URLExt } from '@jupyterlab/coreutils';
7 |
8 | import { FileBrowser } from '@jupyterlab/filebrowser';
9 |
10 | import { refreshIcon } from '@jupyterlab/ui-components';
11 |
12 | import { find } from '@lumino/algorithm';
13 |
14 | import { Message } from '@lumino/messaging';
15 |
16 | import { ISignal, Signal } from '@lumino/signaling';
17 |
18 | import { PanelLayout, Widget } from '@lumino/widgets';
19 |
20 | import { GitHubDrive, parsePath } from './contents';
21 |
22 | /**
23 | * The base url for a mybinder deployment.
24 | */
25 | const MY_BINDER_BASE_URL = 'https://mybinder.org/v2/gh';
26 |
27 | /**
28 | * The className for disabling the mybinder button.
29 | */
30 | const MY_BINDER_DISABLED = 'jp-MyBinderButton-disabled';
31 |
32 | /**
33 | * Widget for hosting the GitHub filebrowser.
34 | */
35 | export class GitHubFileBrowser extends Widget {
36 | constructor(browser: FileBrowser, drive: GitHubDrive) {
37 | super();
38 | this.addClass('jp-GitHubBrowser');
39 | this.layout = new PanelLayout();
40 | (this.layout as PanelLayout).addWidget(browser);
41 | this._browser = browser;
42 | this._drive = drive;
43 |
44 | // Create an editable name for the user/org name.
45 | this.userName = new GitHubUserInput();
46 | this.userName.node.title = 'Click to edit user/organization';
47 | this._browser.toolbar.addItem('user', this.userName);
48 | this.userName.nameChanged.connect(this._onUserChanged, this);
49 | // Create a button that opens GitHub at the appropriate
50 | // repo+directory.
51 | this._openGitHubButton = new ToolbarButton({
52 | onClick: () => {
53 | let url = this._drive.baseUrl;
54 | // If there is no valid user, open the GitHub homepage.
55 | if (!this._drive.validUser) {
56 | window.open(url);
57 | return;
58 | }
59 | const localPath =
60 | this._browser.model.manager.services.contents.localPath(
61 | this._browser.model.path
62 | );
63 | const resource = parsePath(localPath);
64 | url = URLExt.join(url, resource.user);
65 | if (resource.repository) {
66 | url = URLExt.join(
67 | url,
68 | resource.repository,
69 | 'tree',
70 | 'master',
71 | resource.path
72 | );
73 | }
74 | window.open(url);
75 | },
76 | iconClass: 'jp-GitHub-icon jp-Icon jp-Icon-16',
77 | tooltip: 'Open this repository on GitHub'
78 | });
79 | this._openGitHubButton.addClass('jp-GitHub-toolbar-item');
80 | this._browser.toolbar.addItem('GitHub', this._openGitHubButton);
81 |
82 | // Create a button the opens MyBinder to the appropriate repo.
83 | this._launchBinderButton = new ToolbarButton({
84 | onClick: () => {
85 | // If binder is not active for this directory, do nothing.
86 | if (!this._binderActive) {
87 | return;
88 | }
89 | const localPath =
90 | this._browser.model.manager.services.contents.localPath(
91 | this._browser.model.path
92 | );
93 | const resource = parsePath(localPath);
94 | const url = URLExt.join(
95 | MY_BINDER_BASE_URL,
96 | resource.user,
97 | resource.repository,
98 | 'master'
99 | );
100 | // Attempt to open using the JupyterLab tree handler
101 | const tree = URLExt.join('lab', 'tree', resource.path);
102 | window.open(url + `?urlpath=${tree}`);
103 | },
104 | tooltip: 'Launch this repository on mybinder.org',
105 | iconClass: 'jp-MyBinderButton jp-Icon jp-Icon-16'
106 | });
107 | this._launchBinderButton.addClass('jp-GitHub-toolbar-item');
108 | this._browser.toolbar.addItem('binder', this._launchBinderButton);
109 |
110 | // Add our own refresh button, since the other one is hidden
111 | // via CSS.
112 | const refresher = new ToolbarButton({
113 | icon: refreshIcon,
114 | onClick: () => {
115 | this._browser.model.refresh();
116 | },
117 | tooltip: 'Refresh File List'
118 | });
119 | refresher.addClass('jp-GitHub-toolbar-item');
120 | this._browser.toolbar.addItem('gh-refresher', refresher);
121 |
122 | // Set up a listener to check if we can launch mybinder.
123 | this._browser.model.pathChanged.connect(this._onPathChanged, this);
124 | // Trigger an initial pathChanged to check for binder state.
125 | this._onPathChanged();
126 |
127 | this._drive.rateLimitedState.changed.connect(this._updateErrorPanel, this);
128 | }
129 |
130 | /**
131 | * An editable widget hosting the current user name.
132 | */
133 | readonly userName: GitHubUserInput;
134 |
135 | /**
136 | * React to a change in user.
137 | */
138 | private _onUserChanged() {
139 | if (this._changeGuard) {
140 | return;
141 | }
142 | this._changeGuard = true;
143 | this._browser.model.cd(`/${this.userName.name}`).then(() => {
144 | this._changeGuard = false;
145 | this._updateErrorPanel();
146 | // Once we have the new listing, maybe give the file listing
147 | // focus. Once the input element is removed, the active element
148 | // appears to revert to document.body. If the user has subsequently
149 | // focused another element, don't focus the browser listing.
150 | if (document.activeElement === document.body) {
151 | const listing = (this._browser.layout as PanelLayout).widgets[1];
152 | listing.node.focus();
153 | }
154 | });
155 | }
156 |
157 | /**
158 | * React to the path changing for the browser.
159 | */
160 | private _onPathChanged(): void {
161 | const localPath = this._browser.model.manager.services.contents.localPath(
162 | this._browser.model.path
163 | );
164 | const resource = parsePath(localPath);
165 |
166 | // If we are not already changing the user name, set it.
167 | if (!this._changeGuard) {
168 | this._changeGuard = true;
169 | this.userName.name = resource.user;
170 | this._changeGuard = false;
171 | this._updateErrorPanel();
172 | }
173 |
174 | // Check for a valid user.
175 | if (!this._drive.validUser) {
176 | this._launchBinderButton.addClass(MY_BINDER_DISABLED);
177 | this._binderActive = false;
178 | return;
179 | }
180 | // Check for a valid repo.
181 | if (!resource.repository) {
182 | this._launchBinderButton.addClass(MY_BINDER_DISABLED);
183 | this._binderActive = false;
184 | return;
185 | }
186 | // If we are in the root of the repository, check for one of
187 | // the special files indicating we can launch the repository on mybinder.
188 | // TODO: If the user navigates to a subdirectory without hitting the root
189 | // of the repository, we will not check for whether the repo is binder-able.
190 | // Figure out some way around this.
191 | if (resource.path === '') {
192 | const item = find(this._browser.model.items(), i => {
193 | return (
194 | i.name === 'requirements.txt' ||
195 | i.name === 'environment.yml' ||
196 | i.name === 'apt.txt' ||
197 | i.name === 'REQUIRE' ||
198 | i.name === 'Dockerfile' ||
199 | (i.name === 'binder' && i.type === 'directory')
200 | );
201 | });
202 | if (item) {
203 | this._launchBinderButton.removeClass(MY_BINDER_DISABLED);
204 | this._binderActive = true;
205 | return;
206 | } else {
207 | this._launchBinderButton.addClass(MY_BINDER_DISABLED);
208 | this._binderActive = false;
209 | return;
210 | }
211 | }
212 | // If we got this far, we are in a subdirectory of a valid
213 | // repository, and should not change the binderActive status.
214 | return;
215 | }
216 |
217 | /**
218 | * React to a change in the validity of the drive.
219 | */
220 | private _updateErrorPanel(): void {
221 | const localPath = this._browser.model.manager.services.contents.localPath(
222 | this._browser.model.path
223 | );
224 | const resource = parsePath(localPath);
225 | const rateLimited = this._drive.rateLimitedState.get();
226 | const validUser = this._drive.validUser;
227 |
228 | // If we currently have an error panel, remove it.
229 | if (this._errorPanel) {
230 | const listing = (this._browser.layout as PanelLayout).widgets[1];
231 | listing.node.removeChild(this._errorPanel.node);
232 | this._errorPanel.dispose();
233 | this._errorPanel = null;
234 | }
235 |
236 | // If we are being rate limited, make an error panel.
237 | if (rateLimited) {
238 | this._errorPanel = new GitHubErrorPanel(
239 | 'You have been rate limited by GitHub! ' +
240 | 'You will need to wait about an hour before ' +
241 | 'continuing'
242 | );
243 | const listing = (this._browser.layout as PanelLayout).widgets[1];
244 | listing.node.appendChild(this._errorPanel.node);
245 | return;
246 | }
247 |
248 | // If we have an invalid user, make an error panel.
249 | if (!validUser) {
250 | const message = resource.user
251 | ? `"${resource.user}" appears to be an invalid user name!`
252 | : 'Please enter a GitHub user name';
253 | this._errorPanel = new GitHubErrorPanel(message);
254 | const listing = (this._browser.layout as PanelLayout).widgets[1];
255 | listing.node.appendChild(this._errorPanel.node);
256 | return;
257 | }
258 | }
259 |
260 | private _browser: FileBrowser;
261 | private _drive: GitHubDrive;
262 | private _errorPanel: GitHubErrorPanel | null = null;
263 | private _openGitHubButton: ToolbarButton;
264 | private _launchBinderButton: ToolbarButton;
265 | private _binderActive = false;
266 | private _changeGuard = false;
267 | }
268 |
269 | /**
270 | * A widget that hosts an editable field,
271 | * used to host the currently active GitHub
272 | * user name.
273 | */
274 | export class GitHubUserInput extends Widget {
275 | constructor() {
276 | super();
277 | this.addClass('jp-GitHubUserInput');
278 | const layout = (this.layout = new PanelLayout());
279 | const wrapper = new Widget();
280 | wrapper.addClass('jp-GitHubUserInput-wrapper');
281 | this._input = document.createElement('input');
282 | this._input.placeholder = 'GitHub User';
283 | this._input.className = 'jp-GitHubUserInput-input';
284 | wrapper.node.appendChild(this._input);
285 | layout.addWidget(wrapper);
286 | }
287 |
288 | /**
289 | * The current name of the field.
290 | */
291 | get name(): string {
292 | return this._name;
293 | }
294 | set name(value: string) {
295 | if (value === this._name) {
296 | return;
297 | }
298 | const old = this._name;
299 | this._name = value;
300 | this._input.value = value;
301 | this._nameChanged.emit({
302 | oldValue: old,
303 | newValue: value
304 | });
305 | }
306 |
307 | /**
308 | * A signal for when the name changes.
309 | */
310 | get nameChanged(): ISignal {
311 | return this._nameChanged;
312 | }
313 |
314 | /**
315 | * Handle the DOM events for the widget.
316 | *
317 | * @param event - The DOM event sent to the widget.
318 | *
319 | * #### Notes
320 | * This method implements the DOM `EventListener` interface and is
321 | * called in response to events on the main area widget's node. It should
322 | * not be called directly by user code.
323 | */
324 | handleEvent(event: KeyboardEvent): void {
325 | switch (event.type) {
326 | case 'keydown':
327 | switch (event.keyCode) {
328 | case 13: // Enter
329 | event.stopPropagation();
330 | event.preventDefault();
331 | this.name = this._input.value;
332 | this._input.blur();
333 | break;
334 | default:
335 | break;
336 | }
337 | break;
338 | case 'blur':
339 | event.stopPropagation();
340 | event.preventDefault();
341 | this.name = this._input.value;
342 | break;
343 | case 'focus':
344 | event.stopPropagation();
345 | event.preventDefault();
346 | this._input.select();
347 | break;
348 | default:
349 | break;
350 | }
351 | }
352 |
353 | /**
354 | * Handle `after-attach` messages for the widget.
355 | */
356 | protected onAfterAttach(msg: Message): void {
357 | this._input.addEventListener('keydown', this);
358 | this._input.addEventListener('blur', this);
359 | this._input.addEventListener('focus', this);
360 | }
361 |
362 | /**
363 | * Handle `before-detach` messages for the widget.
364 | */
365 | protected onBeforeDetach(msg: Message): void {
366 | this._input.removeEventListener('keydown', this);
367 | this._input.removeEventListener('blur', this);
368 | this._input.removeEventListener('focus', this);
369 | }
370 |
371 | private _name = '';
372 | private _nameChanged = new Signal<
373 | this,
374 | { newValue: string; oldValue: string }
375 | >(this);
376 | private _input: HTMLInputElement;
377 | }
378 |
379 | /**
380 | * A widget hosting an error panel for the browser,
381 | * used if there is an invalid user name or if we
382 | * are being rate-limited.
383 | */
384 | export class GitHubErrorPanel extends Widget {
385 | constructor(message: string) {
386 | super();
387 | this.addClass('jp-GitHubErrorPanel');
388 | const image = document.createElement('div');
389 | const text = document.createElement('div');
390 | image.className = 'jp-GitHubErrorImage';
391 | text.className = 'jp-GitHubErrorText';
392 | text.textContent = message;
393 | this.node.appendChild(image);
394 | this.node.appendChild(text);
395 | }
396 | }
397 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | /*
2 | 👋 Hi! This file was autogenerated by tslint-to-eslint-config.
3 | https://github.com/typescript-eslint/tslint-to-eslint-config
4 |
5 | It represents the closest reasonable ESLint configuration to this
6 | project's original TSLint configuration.
7 |
8 | We recommend eventually switching this configuration to extend from
9 | the recommended rulesets in typescript-eslint.
10 | https://github.com/typescript-eslint/tslint-to-eslint-config/blob/master/docs/FAQs.md
11 |
12 | Happy linting! 💖
13 | */
14 | module.exports = {
15 | env: {
16 | browser: true,
17 | es6: true
18 | },
19 | extends: [
20 | 'eslint:recommended',
21 | 'plugin:@typescript-eslint/eslint-recommended',
22 | 'plugin:@typescript-eslint/recommended',
23 | 'plugin:prettier/recommended'
24 | ],
25 | ignorePatterns: ['node_modules', 'dist', 'coverage', '**/*.d.ts', 'tests'],
26 | parser: '@typescript-eslint/parser',
27 | parserOptions: {
28 | project: 'tsconfig.json',
29 | sourceType: 'module'
30 | },
31 | plugins: [
32 | 'eslint-plugin-import',
33 | 'eslint-plugin-no-null',
34 | '@typescript-eslint'
35 | ],
36 | root: true,
37 | rules: {
38 | '@babel/object-curly-spacing': 'off',
39 | '@babel/semi': 'off',
40 | '@typescript-eslint/adjacent-overload-signatures': 'error',
41 | '@typescript-eslint/ban-ts-comment': 'error',
42 | '@typescript-eslint/ban-types': 'error',
43 | '@typescript-eslint/block-spacing': 'off',
44 | '@typescript-eslint/brace-style': 'off',
45 | '@typescript-eslint/comma-dangle': 'off',
46 | '@typescript-eslint/comma-spacing': 'off',
47 | '@typescript-eslint/consistent-type-assertions': 'error',
48 | '@typescript-eslint/dot-notation': 'off',
49 | '@typescript-eslint/explicit-function-return-type': 'off',
50 | '@typescript-eslint/explicit-member-accessibility': [
51 | 'off',
52 | {
53 | accessibility: 'explicit'
54 | }
55 | ],
56 | '@typescript-eslint/explicit-module-boundary-types': 'off',
57 | '@typescript-eslint/func-call-spacing': 'off',
58 | '@typescript-eslint/key-spacing': 'off',
59 | '@typescript-eslint/keyword-spacing': 'off',
60 | '@typescript-eslint/lines-around-comment': 'off',
61 | '@typescript-eslint/member-delimiter-style': [
62 | 'error',
63 | {
64 | multiline: {
65 | delimiter: 'semi',
66 | requireLast: true
67 | },
68 | singleline: {
69 | delimiter: 'semi',
70 | requireLast: false
71 | }
72 | }
73 | ],
74 | '@typescript-eslint/member-ordering': 'off',
75 | '@typescript-eslint/naming-convention': [
76 | 'error',
77 | {
78 | selector: 'variable',
79 | format: ['camelCase', 'UPPER_CASE'],
80 | leadingUnderscore: 'allow',
81 | trailingUnderscore: 'forbid'
82 | }
83 | ],
84 | '@typescript-eslint/no-array-constructor': 'error',
85 | '@typescript-eslint/no-empty-function': 'error',
86 | '@typescript-eslint/no-empty-interface': 'error',
87 | '@typescript-eslint/no-explicit-any': 'off',
88 | '@typescript-eslint/no-extra-non-null-assertion': 'error',
89 | '@typescript-eslint/no-extra-parens': 'off',
90 | '@typescript-eslint/no-extra-semi': 'off',
91 | '@typescript-eslint/no-inferrable-types': 'off',
92 | '@typescript-eslint/no-loss-of-precision': 'error',
93 | '@typescript-eslint/no-misused-new': 'error',
94 | '@typescript-eslint/no-namespace': 'off',
95 | '@typescript-eslint/no-non-null-asserted-optional-chain': 'error',
96 | '@typescript-eslint/no-non-null-assertion': 'warn',
97 | '@typescript-eslint/no-require-imports': 'off',
98 | '@typescript-eslint/no-shadow': [
99 | 'off',
100 | {
101 | hoist: 'all'
102 | }
103 | ],
104 | '@typescript-eslint/no-this-alias': 'error',
105 | '@typescript-eslint/no-unnecessary-type-constraint': 'error',
106 | '@typescript-eslint/no-unused-expressions': 'error',
107 | '@typescript-eslint/no-unused-vars': [
108 | 'warn',
109 | {
110 | args: 'none'
111 | }
112 | ],
113 | '@typescript-eslint/no-use-before-define': 'off',
114 | '@typescript-eslint/no-var-requires': 'error',
115 | '@typescript-eslint/object-curly-spacing': 'off',
116 | '@typescript-eslint/prefer-as-const': 'error',
117 | '@typescript-eslint/prefer-namespace-keyword': 'error',
118 | '@typescript-eslint/quotes': [
119 | 'error',
120 | 'single',
121 | {
122 | avoidEscape: true
123 | }
124 | ],
125 | '@typescript-eslint/semi': ['error', 'always'],
126 | '@typescript-eslint/space-before-blocks': 'off',
127 | '@typescript-eslint/space-before-function-paren': 'off',
128 | '@typescript-eslint/space-infix-ops': 'off',
129 | '@typescript-eslint/triple-slash-reference': 'error',
130 | '@typescript-eslint/type-annotation-spacing': 'off',
131 | '@typescript-eslint/typedef': 'off',
132 | 'array-bracket-newline': 'off',
133 | 'array-bracket-spacing': 'off',
134 | 'array-element-newline': 'off',
135 | 'arrow-body-style': 'off',
136 | 'arrow-parens': 'off',
137 | 'arrow-spacing': 'off',
138 | 'babel/object-curly-spacing': 'off',
139 | 'babel/quotes': 'off',
140 | 'babel/semi': 'off',
141 | 'block-spacing': 'off',
142 | 'brace-style': ['error', '1tbs'],
143 | 'comma-dangle': 'off',
144 | 'comma-spacing': 'off',
145 | 'comma-style': 'off',
146 | 'computed-property-spacing': 'off',
147 | 'constructor-super': 'error',
148 | curly: 'error',
149 | 'default-case': 'error',
150 | 'dot-location': 'off',
151 | 'dot-notation': 'off',
152 | 'eol-last': 'error',
153 | eqeqeq: ['error', 'smart'],
154 | 'flowtype/boolean-style': 'off',
155 | 'flowtype/delimiter-dangle': 'off',
156 | 'flowtype/generic-spacing': 'off',
157 | 'flowtype/object-type-curly-spacing': 'off',
158 | 'flowtype/object-type-delimiter': 'off',
159 | 'flowtype/quotes': 'off',
160 | 'flowtype/semi': 'off',
161 | 'flowtype/space-after-type-colon': 'off',
162 | 'flowtype/space-before-generic-bracket': 'off',
163 | 'flowtype/space-before-type-colon': 'off',
164 | 'flowtype/union-intersection-spacing': 'off',
165 | 'for-direction': 'error',
166 | 'func-call-spacing': 'off',
167 | 'function-call-argument-newline': 'off',
168 | 'function-paren-newline': 'off',
169 | 'generator-star': 'off',
170 | 'generator-star-spacing': 'off',
171 | 'getter-return': 'error',
172 | 'guard-for-in': 'off',
173 | 'id-denylist': [
174 | 'error',
175 | 'any',
176 | 'Number',
177 | 'number',
178 | 'String',
179 | 'string',
180 | 'Boolean',
181 | 'boolean',
182 | 'Undefined',
183 | 'undefined'
184 | ],
185 | 'id-match': 'error',
186 | 'implicit-arrow-linebreak': 'off',
187 | 'import/no-default-export': 'off',
188 | indent: 'off',
189 | 'indent-legacy': 'off',
190 | 'jsx-quotes': 'off',
191 | 'key-spacing': 'off',
192 | 'keyword-spacing': 'off',
193 | 'linebreak-style': 'off',
194 | 'lines-around-comment': 'off',
195 | 'max-len': 'off',
196 | 'multiline-ternary': 'off',
197 | 'new-parens': 'error',
198 | 'newline-per-chained-call': 'off',
199 | 'no-array-constructor': 'off',
200 | 'no-arrow-condition': 'off',
201 | 'no-async-promise-executor': 'error',
202 | 'no-bitwise': 'error',
203 | 'no-caller': 'error',
204 | 'no-case-declarations': 'error',
205 | 'no-class-assign': 'error',
206 | 'no-comma-dangle': 'off',
207 | 'no-compare-neg-zero': 'error',
208 | 'no-cond-assign': 'error',
209 | 'no-confusing-arrow': 'off',
210 | 'no-console': [
211 | 'error',
212 | {
213 | allow: [
214 | 'log',
215 | 'warn',
216 | 'dir',
217 | 'timeLog',
218 | 'assert',
219 | 'clear',
220 | 'count',
221 | 'countReset',
222 | 'group',
223 | 'groupEnd',
224 | 'table',
225 | 'dirxml',
226 | 'error',
227 | 'groupCollapsed',
228 | 'Console',
229 | 'profile',
230 | 'profileEnd',
231 | 'timeStamp',
232 | 'context'
233 | ]
234 | }
235 | ],
236 | 'no-const-assign': 'error',
237 | 'no-constant-condition': 'error',
238 | 'no-control-regex': 'error',
239 | 'no-debugger': 'error',
240 | 'no-delete-var': 'error',
241 | 'no-dupe-args': 'error',
242 | 'no-dupe-class-members': 'error',
243 | 'no-dupe-else-if': 'error',
244 | 'no-dupe-keys': 'error',
245 | 'no-duplicate-case': 'error',
246 | 'no-empty': 'error',
247 | 'no-empty-character-class': 'error',
248 | 'no-empty-function': 'off',
249 | 'no-empty-pattern': 'error',
250 | 'no-eval': 'error',
251 | 'no-ex-assign': 'error',
252 | 'no-extra-boolean-cast': 'error',
253 | 'no-extra-parens': 'off',
254 | 'no-extra-semi': 'off',
255 | 'no-fallthrough': 'error',
256 | 'no-floating-decimal': 'off',
257 | 'no-func-assign': 'error',
258 | 'no-global-assign': 'error',
259 | 'no-import-assign': 'error',
260 | 'no-inner-declarations': 'error',
261 | 'no-invalid-regexp': 'error',
262 | 'no-invalid-this': 'error',
263 | 'no-irregular-whitespace': 'error',
264 | 'no-loss-of-precision': 'off',
265 | 'no-misleading-character-class': 'error',
266 | 'no-mixed-operators': 'off',
267 | 'no-mixed-spaces-and-tabs': 'off',
268 | 'no-multi-spaces': 'off',
269 | 'no-multiple-empty-lines': 'off',
270 | 'no-new-symbol': 'error',
271 | 'no-new-wrappers': 'error',
272 | 'no-nonoctal-decimal-escape': 'error',
273 | 'no-null/no-null': 'off',
274 | 'no-obj-calls': 'error',
275 | 'no-octal': 'error',
276 | 'no-prototype-builtins': 'error',
277 | 'no-redeclare': 'error',
278 | 'no-regex-spaces': 'error',
279 | 'no-reserved-keys': 'off',
280 | 'no-self-assign': 'error',
281 | 'no-setter-return': 'error',
282 | 'no-shadow': 'off',
283 | 'no-shadow-restricted-names': 'error',
284 | 'no-space-before-semi': 'off',
285 | 'no-spaced-func': 'off',
286 | 'no-sparse-arrays': 'error',
287 | 'no-tabs': 'off',
288 | 'no-this-before-super': 'error',
289 | 'no-trailing-spaces': 'error',
290 | 'no-undef': 'error',
291 | 'no-underscore-dangle': 'off',
292 | 'no-unexpected-multiline': 'off',
293 | 'no-unreachable': 'error',
294 | 'no-unsafe-finally': 'error',
295 | 'no-unsafe-negation': 'error',
296 | 'no-unsafe-optional-chaining': 'error',
297 | 'no-unused-expressions': 'off',
298 | 'no-unused-labels': 'error',
299 | 'no-unused-vars': 'off',
300 | 'no-use-before-define': 'off',
301 | 'no-useless-backreference': 'error',
302 | 'no-useless-catch': 'error',
303 | 'no-useless-escape': 'error',
304 | 'no-var': 'error',
305 | 'no-whitespace-before-property': 'off',
306 | 'no-with': 'error',
307 | 'no-wrap-func': 'off',
308 | 'nonblock-statement-body-position': 'off',
309 | 'object-curly-newline': 'off',
310 | 'object-curly-spacing': 'off',
311 | 'object-property-newline': 'off',
312 | 'one-var': ['error', 'never'],
313 | 'one-var-declaration-per-line': 'off',
314 | 'operator-linebreak': 'off',
315 | 'padded-blocks': 'off',
316 | 'prefer-arrow-callback': 'error',
317 | 'prettier/prettier': 'error',
318 | 'quote-props': 'off',
319 | quotes: 'off',
320 | radix: 'error',
321 | 'react/jsx-child-element-spacing': 'off',
322 | 'react/jsx-closing-bracket-location': 'off',
323 | 'react/jsx-closing-tag-location': 'off',
324 | 'react/jsx-curly-newline': 'off',
325 | 'react/jsx-curly-spacing': 'off',
326 | 'react/jsx-equals-spacing': 'off',
327 | 'react/jsx-first-prop-new-line': 'off',
328 | 'react/jsx-indent': 'off',
329 | 'react/jsx-indent-props': 'off',
330 | 'react/jsx-max-props-per-line': 'off',
331 | 'react/jsx-newline': 'off',
332 | 'react/jsx-one-expression-per-line': 'off',
333 | 'react/jsx-props-no-multi-spaces': 'off',
334 | 'react/jsx-space-before-closing': 'off',
335 | 'react/jsx-tag-spacing': 'off',
336 | 'react/jsx-wrap-multilines': 'off',
337 | 'require-yield': 'error',
338 | 'rest-spread-spacing': 'off',
339 | semi: 'off',
340 | 'semi-spacing': 'off',
341 | 'semi-style': 'off',
342 | 'space-after-function-name': 'off',
343 | 'space-after-keywords': 'off',
344 | 'space-before-blocks': 'off',
345 | 'space-before-function-paren': 'off',
346 | 'space-before-function-parentheses': 'off',
347 | 'space-before-keywords': 'off',
348 | 'space-in-brackets': 'off',
349 | 'space-in-parens': 'off',
350 | 'space-infix-ops': 'off',
351 | 'space-return-throw-case': 'off',
352 | 'space-unary-ops': 'off',
353 | 'space-unary-word-ops': 'off',
354 | 'spaced-comment': [
355 | 'error',
356 | 'always',
357 | {
358 | markers: ['/']
359 | }
360 | ],
361 | 'standard/array-bracket-even-spacing': 'off',
362 | 'standard/computed-property-even-spacing': 'off',
363 | 'standard/object-curly-even-spacing': 'off',
364 | 'switch-colon-spacing': 'off',
365 | 'template-curly-spacing': 'off',
366 | 'template-tag-spacing': 'off',
367 | 'unicode-bom': 'off',
368 | 'unicorn/empty-brace-spaces': 'off',
369 | 'unicorn/no-nested-ternary': 'off',
370 | 'unicorn/number-literal-case': 'off',
371 | 'use-isnan': 'error',
372 | 'valid-typeof': 'error',
373 | 'vue/array-bracket-newline': 'off',
374 | 'vue/array-bracket-spacing': 'off',
375 | 'vue/arrow-spacing': 'off',
376 | 'vue/block-spacing': 'off',
377 | 'vue/block-tag-newline': 'off',
378 | 'vue/brace-style': 'off',
379 | 'vue/comma-dangle': 'off',
380 | 'vue/comma-spacing': 'off',
381 | 'vue/comma-style': 'off',
382 | 'vue/dot-location': 'off',
383 | 'vue/func-call-spacing': 'off',
384 | 'vue/html-closing-bracket-newline': 'off',
385 | 'vue/html-closing-bracket-spacing': 'off',
386 | 'vue/html-end-tags': 'off',
387 | 'vue/html-indent': 'off',
388 | 'vue/html-quotes': 'off',
389 | 'vue/html-self-closing': 'off',
390 | 'vue/key-spacing': 'off',
391 | 'vue/keyword-spacing': 'off',
392 | 'vue/max-attributes-per-line': 'off',
393 | 'vue/max-len': 'off',
394 | 'vue/multiline-html-element-content-newline': 'off',
395 | 'vue/multiline-ternary': 'off',
396 | 'vue/mustache-interpolation-spacing': 'off',
397 | 'vue/no-extra-parens': 'off',
398 | 'vue/no-multi-spaces': 'off',
399 | 'vue/no-spaces-around-equal-signs-in-attribute': 'off',
400 | 'vue/object-curly-newline': 'off',
401 | 'vue/object-curly-spacing': 'off',
402 | 'vue/object-property-newline': 'off',
403 | 'vue/operator-linebreak': 'off',
404 | 'vue/quote-props': 'off',
405 | 'vue/script-indent': 'off',
406 | 'vue/singleline-html-element-content-newline': 'off',
407 | 'vue/space-in-parens': 'off',
408 | 'vue/space-infix-ops': 'off',
409 | 'vue/space-unary-ops': 'off',
410 | 'vue/template-curly-spacing': 'off',
411 | 'wrap-iife': 'off',
412 | 'wrap-regex': 'off',
413 | 'yield-star-spacing': 'off'
414 | }
415 | };
416 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 |
4 |
5 | ## 4.0.0
6 |
7 | ([Full Changelog](https://github.com/jupyterlab/jupyterlab-github/compare/v3.0.1...5cff452e669571d6da9ad5874368b9fe793d567a))
8 |
9 | ### Maintenance and upkeep improvements
10 |
11 | - Rename default branch to `main` [#148](https://github.com/jupyterlab/jupyterlab-github/pull/148) ([@jtpio](https://github.com/jtpio))
12 | - Upgrade extension to JupyterLab 4 [#145](https://github.com/jupyterlab/jupyterlab-github/pull/145) ([@mahendrapaipuri](https://github.com/mahendrapaipuri))
13 | - Optional ILayoutRestorer [#128](https://github.com/jupyterlab/jupyterlab-github/pull/128) ([@jtpio](https://github.com/jtpio))
14 | - Add the release environment to the release workflow [#152](https://github.com/jupyterlab/jupyterlab-github/pull/152) ([@jtpio](https://github.com/jtpio))
15 | - Fix publish workflow [#150](https://github.com/jupyterlab/jupyterlab-github/pull/150) ([@jtpio](https://github.com/jtpio))
16 |
17 | ### Contributors to this release
18 |
19 | ([GitHub contributors page for this release](https://github.com/jupyterlab/jupyterlab-github/graphs/contributors?from=2021-11-27&to=2023-08-03&type=c))
20 |
21 | [@github-actions](https://github.com/search?q=repo%3Ajupyterlab%2Fjupyterlab-github+involves%3Agithub-actions+updated%3A2021-11-27..2023-08-03&type=Issues) | [@jtpio](https://github.com/search?q=repo%3Ajupyterlab%2Fjupyterlab-github+involves%3Ajtpio+updated%3A2021-11-27..2023-08-03&type=Issues) | [@mahendrapaipuri](https://github.com/search?q=repo%3Ajupyterlab%2Fjupyterlab-github+involves%3Amahendrapaipuri+updated%3A2021-11-27..2023-08-03&type=Issues) | [@welcome](https://github.com/search?q=repo%3Ajupyterlab%2Fjupyterlab-github+involves%3Awelcome+updated%3A2021-11-27..2023-08-03&type=Issues)
22 |
23 |
24 |
25 | ## v3.0.1
26 |
27 | ([full changelog](https://github.com/jupyterlab/jupyterlab-github/compare/bdf7f93...fe08169))
28 |
29 | ### Merged PRs
30 |
31 | - Release 3.0.1 to pick up updated readme file [#127](https://github.com/jupyterlab/jupyterlab-github/pull/127) ([@krassowski](https://github.com/krassowski))
32 | - Prepare 3.0.0 release to PyPI with prebuilt extension [#126](https://github.com/jupyterlab/jupyterlab-github/pull/126) ([@krassowski](https://github.com/krassowski))
33 | - Bump browserslist from 4.16.3 to 4.17.4 [#124](https://github.com/jupyterlab/jupyterlab-github/pull/124) ([@dependabot](https://github.com/dependabot))
34 | - Bump glob-parent from 5.1.1 to 5.1.2 [#123](https://github.com/jupyterlab/jupyterlab-github/pull/123) ([@dependabot](https://github.com/dependabot))
35 | - Bump postcss from 7.0.35 to 7.0.39 [#122](https://github.com/jupyterlab/jupyterlab-github/pull/122) ([@dependabot](https://github.com/dependabot))
36 | - Bump ws from 7.2.1 to 7.5.5 [#121](https://github.com/jupyterlab/jupyterlab-github/pull/121) ([@dependabot](https://github.com/dependabot))
37 | - Bump normalize-url from 4.5.0 to 4.5.1 [#120](https://github.com/jupyterlab/jupyterlab-github/pull/120) ([@dependabot](https://github.com/dependabot))
38 | - Bump tar from 6.1.0 to 6.1.11 [#119](https://github.com/jupyterlab/jupyterlab-github/pull/119) ([@dependabot](https://github.com/dependabot))
39 | - Bump path-parse from 1.0.6 to 1.0.7 [#117](https://github.com/jupyterlab/jupyterlab-github/pull/117) ([@dependabot](https://github.com/dependabot))
40 | - Bump hosted-git-info from 2.8.8 to 2.8.9 [#114](https://github.com/jupyterlab/jupyterlab-github/pull/114) ([@dependabot](https://github.com/dependabot))
41 | - Bump lodash from 4.17.15 to 4.17.21 [#113](https://github.com/jupyterlab/jupyterlab-github/pull/113) ([@dependabot](https://github.com/dependabot))
42 | - JupyterLab 3.0 [#112](https://github.com/jupyterlab/jupyterlab-github/pull/112) ([@khwj](https://github.com/khwj))
43 | - Bump node-fetch from 2.6.0 to 2.6.1 [#108](https://github.com/jupyterlab/jupyterlab-github/pull/108) ([@dependabot](https://github.com/dependabot))
44 | - Jupyterlab 2.0 [#98](https://github.com/jupyterlab/jupyterlab-github/pull/98) ([@ian-r-rose](https://github.com/ian-r-rose))
45 |
46 | ### Contributors to this release
47 |
48 | ([GitHub contributors page for this release](https://github.com/jupyterlab/jupyterlab-github/graphs/contributors?from=2019-07-26&to=2021-11-27&type=c))
49 |
50 | [@dependabot](https://github.com/search?q=repo%3Ajupyterlab%2Fjupyterlab-github+involves%3Adependabot+updated%3A2019-07-26..2021-11-27&type=Issues) | [@ian-r-rose](https://github.com/search?q=repo%3Ajupyterlab%2Fjupyterlab-github+involves%3Aian-r-rose+updated%3A2019-07-26..2021-11-27&type=Issues) | [@jhgoebbert](https://github.com/search?q=repo%3Ajupyterlab%2Fjupyterlab-github+involves%3Ajhgoebbert+updated%3A2019-07-26..2021-11-27&type=Issues) | [@khwj](https://github.com/search?q=repo%3Ajupyterlab%2Fjupyterlab-github+involves%3Akhwj+updated%3A2019-07-26..2021-11-27&type=Issues) | [@krassowski](https://github.com/search?q=repo%3Ajupyterlab%2Fjupyterlab-github+involves%3Akrassowski+updated%3A2019-07-26..2021-11-27&type=Issues) | [@welcome](https://github.com/search?q=repo%3Ajupyterlab%2Fjupyterlab-github+involves%3Awelcome+updated%3A2019-07-26..2021-11-27&type=Issues)
51 |
52 | ## v1.0.1
53 |
54 | ([full changelog](https://github.com/jupyterlab/jupyterlab-github/compare/a7f02c9...bdf7f93))
55 |
56 | ### Bugs fixed
57 |
58 | - The repo-to-directory conversion should include the owner in the path. [#89](https://github.com/jupyterlab/jupyterlab-github/pull/89) ([@ian-r-rose](https://github.com/ian-r-rose))
59 |
60 | ### Other merged PRs
61 |
62 | - Bump lodash from 4.17.11 to 4.17.14 [#88](https://github.com/jupyterlab/jupyterlab-github/pull/88) ([@dependabot](https://github.com/dependabot))
63 | - Bump lodash.mergewith from 4.6.1 to 4.6.2 [#87](https://github.com/jupyterlab/jupyterlab-github/pull/87) ([@dependabot](https://github.com/dependabot))
64 |
65 | ### Contributors to this release
66 |
67 | ([GitHub contributors page for this release](https://github.com/jupyterlab/jupyterlab-github/graphs/contributors?from=2019-07-08&to=2019-07-26&type=c))
68 |
69 | [@dependabot](https://github.com/search?q=repo%3Ajupyterlab%2Fjupyterlab-github+involves%3Adependabot+updated%3A2019-07-08..2019-07-26&type=Issues) | [@ian-r-rose](https://github.com/search?q=repo%3Ajupyterlab%2Fjupyterlab-github+involves%3Aian-r-rose+updated%3A2019-07-08..2019-07-26&type=Issues)
70 |
71 | ## v1.0.0
72 |
73 | ([full changelog](https://github.com/jupyterlab/jupyterlab-github/compare/fb157f7...a7f02c9))
74 |
75 | ### Maintenance and upkeep improvements
76 |
77 | - Simplify input logic for username selector, avoids weird swapping of DOM elements. [#78](https://github.com/jupyterlab/jupyterlab-github/pull/78) ([@ian-r-rose](https://github.com/ian-r-rose))
78 | - Update for JLab 1.0 [#77](https://github.com/jupyterlab/jupyterlab-github/pull/77) ([@ian-r-rose](https://github.com/ian-r-rose))
79 |
80 | ### Other merged PRs
81 |
82 | - Remove deprecated OAuth authentication. [#86](https://github.com/jupyterlab/jupyterlab-github/pull/86) ([@ian-r-rose](https://github.com/ian-r-rose))
83 | - Bump js-yaml from 3.12.0 to 3.13.1 [#85](https://github.com/jupyterlab/jupyterlab-github/pull/85) ([@dependabot](https://github.com/dependabot))
84 | - Update to JupyterLab 1.0.0-alpha.6 [#82](https://github.com/jupyterlab/jupyterlab-github/pull/82) ([@lresende](https://github.com/lresende))
85 | - Remove duplicated text [#81](https://github.com/jupyterlab/jupyterlab-github/pull/81) ([@gcbeltramini](https://github.com/gcbeltramini))
86 |
87 | ### Contributors to this release
88 |
89 | ([GitHub contributors page for this release](https://github.com/jupyterlab/jupyterlab-github/graphs/contributors?from=2018-10-05&to=2019-07-08&type=c))
90 |
91 | [@dependabot](https://github.com/search?q=repo%3Ajupyterlab%2Fjupyterlab-github+involves%3Adependabot+updated%3A2018-10-05..2019-07-08&type=Issues) | [@dhirschfeld](https://github.com/search?q=repo%3Ajupyterlab%2Fjupyterlab-github+involves%3Adhirschfeld+updated%3A2018-10-05..2019-07-08&type=Issues) | [@gcbeltramini](https://github.com/search?q=repo%3Ajupyterlab%2Fjupyterlab-github+involves%3Agcbeltramini+updated%3A2018-10-05..2019-07-08&type=Issues) | [@ian-r-rose](https://github.com/search?q=repo%3Ajupyterlab%2Fjupyterlab-github+involves%3Aian-r-rose+updated%3A2018-10-05..2019-07-08&type=Issues) | [@lresende](https://github.com/search?q=repo%3Ajupyterlab%2Fjupyterlab-github+involves%3Alresende+updated%3A2018-10-05..2019-07-08&type=Issues)
92 |
93 | ## v0.10.0
94 |
95 | ([full changelog](https://github.com/jupyterlab/jupyterlab-github/compare/ebd6d9a...fb157f7))
96 |
97 | ### Enhancements made
98 |
99 | - Allow setting the accessToken from JupyterLab [#70](https://github.com/jupyterlab/jupyterlab-github/pull/70) ([@dhirschfeld](https://github.com/dhirschfeld))
100 |
101 | ### Maintenance and upkeep improvements
102 |
103 | - Updates for JupyterLab v0.35. [#73](https://github.com/jupyterlab/jupyterlab-github/pull/73) ([@ian-r-rose](https://github.com/ian-r-rose))
104 | - Updates for JupyterLab 0.34 [#67](https://github.com/jupyterlab/jupyterlab-github/pull/67) ([@ian-r-rose](https://github.com/ian-r-rose))
105 |
106 | ### Contributors to this release
107 |
108 | ([GitHub contributors page for this release](https://github.com/jupyterlab/jupyterlab-github/graphs/contributors?from=2018-08-18&to=2018-10-05&type=c))
109 |
110 | [@dhirschfeld](https://github.com/search?q=repo%3Ajupyterlab%2Fjupyterlab-github+involves%3Adhirschfeld+updated%3A2018-08-18..2018-10-05&type=Issues) | [@ellisonbg](https://github.com/search?q=repo%3Ajupyterlab%2Fjupyterlab-github+involves%3Aellisonbg+updated%3A2018-08-18..2018-10-05&type=Issues) | [@firasm](https://github.com/search?q=repo%3Ajupyterlab%2Fjupyterlab-github+involves%3Afirasm+updated%3A2018-08-18..2018-10-05&type=Issues) | [@fm75](https://github.com/search?q=repo%3Ajupyterlab%2Fjupyterlab-github+involves%3Afm75+updated%3A2018-08-18..2018-10-05&type=Issues) | [@ian-r-rose](https://github.com/search?q=repo%3Ajupyterlab%2Fjupyterlab-github+involves%3Aian-r-rose+updated%3A2018-08-18..2018-10-05&type=Issues)
111 |
112 | ## v0.9.0
113 |
114 | ([full changelog](https://github.com/jupyterlab/jupyterlab-github/compare/a1bdbac...ebd6d9a))
115 |
116 | ### Maintenance and upkeep improvements
117 |
118 | - Updates for 0.33 [#60](https://github.com/jupyterlab/jupyterlab-github/pull/60) ([@ian-r-rose](https://github.com/ian-r-rose))
119 |
120 | ### Other merged PRs
121 |
122 | - Update .gitignore [#65](https://github.com/jupyterlab/jupyterlab-github/pull/65) ([@dhirschfeld](https://github.com/dhirschfeld))
123 |
124 | ### Contributors to this release
125 |
126 | ([GitHub contributors page for this release](https://github.com/jupyterlab/jupyterlab-github/graphs/contributors?from=2018-07-25&to=2018-08-18&type=c))
127 |
128 | [@dhirschfeld](https://github.com/search?q=repo%3Ajupyterlab%2Fjupyterlab-github+involves%3Adhirschfeld+updated%3A2018-07-25..2018-08-18&type=Issues) | [@ellisonbg](https://github.com/search?q=repo%3Ajupyterlab%2Fjupyterlab-github+involves%3Aellisonbg+updated%3A2018-07-25..2018-08-18&type=Issues) | [@ian-r-rose](https://github.com/search?q=repo%3Ajupyterlab%2Fjupyterlab-github+involves%3Aian-r-rose+updated%3A2018-07-25..2018-08-18&type=Issues)
129 |
130 | ## v0.8.0
131 |
132 | ([full changelog](https://github.com/jupyterlab/jupyterlab-github/compare/08ca51c...a1bdbac))
133 |
134 | ### Merged PRs
135 |
136 | - copy request for get_next_page [#63](https://github.com/jupyterlab/jupyterlab-github/pull/63) ([@ktong](https://github.com/ktong))
137 | - Include LICENSE file in wheels [#58](https://github.com/jupyterlab/jupyterlab-github/pull/58) ([@toddrme2178](https://github.com/toddrme2178))
138 |
139 | ### Contributors to this release
140 |
141 | ([GitHub contributors page for this release](https://github.com/jupyterlab/jupyterlab-github/graphs/contributors?from=2018-05-14&to=2018-07-25&type=c))
142 |
143 | [@gabrielvrl](https://github.com/search?q=repo%3Ajupyterlab%2Fjupyterlab-github+involves%3Agabrielvrl+updated%3A2018-05-14..2018-07-25&type=Issues) | [@ian-r-rose](https://github.com/search?q=repo%3Ajupyterlab%2Fjupyterlab-github+involves%3Aian-r-rose+updated%3A2018-05-14..2018-07-25&type=Issues) | [@ktong](https://github.com/search?q=repo%3Ajupyterlab%2Fjupyterlab-github+involves%3Aktong+updated%3A2018-05-14..2018-07-25&type=Issues) | [@michaelaye](https://github.com/search?q=repo%3Ajupyterlab%2Fjupyterlab-github+involves%3Amichaelaye+updated%3A2018-05-14..2018-07-25&type=Issues) | [@toddrme2178](https://github.com/search?q=repo%3Ajupyterlab%2Fjupyterlab-github+involves%3Atoddrme2178+updated%3A2018-05-14..2018-07-25&type=Issues)
144 |
145 | ## v0.7.2
146 |
147 | ([full changelog](https://github.com/jupyterlab/jupyterlab-github/compare/7a9550d...08ca51c))
148 |
149 | ### Bugs fixed
150 |
151 | - Fix problem with redirects in Safari by removing spurious slash. [#56](https://github.com/jupyterlab/jupyterlab-github/pull/56) ([@ian-r-rose](https://github.com/ian-r-rose))
152 |
153 | ### Contributors to this release
154 |
155 | ([GitHub contributors page for this release](https://github.com/jupyterlab/jupyterlab-github/graphs/contributors?from=2018-04-23&to=2018-05-14&type=c))
156 |
157 | [@gabrielvrl](https://github.com/search?q=repo%3Ajupyterlab%2Fjupyterlab-github+involves%3Agabrielvrl+updated%3A2018-04-23..2018-05-14&type=Issues) | [@ian-r-rose](https://github.com/search?q=repo%3Ajupyterlab%2Fjupyterlab-github+involves%3Aian-r-rose+updated%3A2018-04-23..2018-05-14&type=Issues) | [@michaelaye](https://github.com/search?q=repo%3Ajupyterlab%2Fjupyterlab-github+involves%3Amichaelaye+updated%3A2018-04-23..2018-05-14&type=Issues)
158 |
159 | ## v0.7.1
160 |
161 | ([full changelog](https://github.com/jupyterlab/jupyterlab-github/compare/66433b2...7a9550d))
162 |
163 | ### Merged PRs
164 |
165 | - Add ability to configure the GitHub base url [#52](https://github.com/jupyterlab/jupyterlab-github/pull/52) ([@dhirschfeld](https://github.com/dhirschfeld))
166 |
167 | ### Contributors to this release
168 |
169 | ([GitHub contributors page for this release](https://github.com/jupyterlab/jupyterlab-github/graphs/contributors?from=2018-04-20&to=2018-04-23&type=c))
170 |
171 | [@dhirschfeld](https://github.com/search?q=repo%3Ajupyterlab%2Fjupyterlab-github+involves%3Adhirschfeld+updated%3A2018-04-20..2018-04-23&type=Issues) | [@ian-r-rose](https://github.com/search?q=repo%3Ajupyterlab%2Fjupyterlab-github+involves%3Aian-r-rose+updated%3A2018-04-20..2018-04-23&type=Issues)
172 |
173 | ## v0.7.0
174 |
175 | ([full changelog](https://github.com/jupyterlab/jupyterlab-github/compare/2fd67f2...66433b2))
176 |
177 | ### Enhancements made
178 |
179 | - Allow setting a default repository at launch. [#50](https://github.com/jupyterlab/jupyterlab-github/pull/50) ([@ian-r-rose](https://github.com/ian-r-rose))
180 | - Allow for private repos to be viewed if the user is authenticated. [#48](https://github.com/jupyterlab/jupyterlab-github/pull/48) ([@ian-r-rose](https://github.com/ian-r-rose))
181 | - Change to using access tokens and make the api url configurable. [#47](https://github.com/jupyterlab/jupyterlab-github/pull/47) ([@dhirschfeld](https://github.com/dhirschfeld))
182 |
183 | ### Bugs fixed
184 |
185 | - Conditionally use err.response. [#51](https://github.com/jupyterlab/jupyterlab-github/pull/51) ([@ian-r-rose](https://github.com/ian-r-rose))
186 |
187 | ### Maintenance and upkeep improvements
188 |
189 | - Updates for JupyterLab v0.32 [#44](https://github.com/jupyterlab/jupyterlab-github/pull/44) ([@ian-r-rose](https://github.com/ian-r-rose))
190 |
191 | ### Other merged PRs
192 |
193 | - Update docs to include access_token instructions. [#49](https://github.com/jupyterlab/jupyterlab-github/pull/49) ([@ian-r-rose](https://github.com/ian-r-rose))
194 |
195 | ### Contributors to this release
196 |
197 | ([GitHub contributors page for this release](https://github.com/jupyterlab/jupyterlab-github/graphs/contributors?from=2018-04-13&to=2018-04-20&type=c))
198 |
199 | [@dhirschfeld](https://github.com/search?q=repo%3Ajupyterlab%2Fjupyterlab-github+involves%3Adhirschfeld+updated%3A2018-04-13..2018-04-20&type=Issues) | [@ian-r-rose](https://github.com/search?q=repo%3Ajupyterlab%2Fjupyterlab-github+involves%3Aian-r-rose+updated%3A2018-04-13..2018-04-20&type=Issues)
200 |
201 | ## v0.6.0
202 |
203 | ([full changelog](https://github.com/jupyterlab/jupyterlab-github/compare/7e506c2...2fd67f2))
204 |
205 | ### Maintenance and upkeep improvements
206 |
207 | - Updates for JupyterLab v0.32 [#44](https://github.com/jupyterlab/jupyterlab-github/pull/44) ([@ian-r-rose](https://github.com/ian-r-rose))
208 |
209 | ### Other merged PRs
210 |
211 | - Use textencoder [#41](https://github.com/jupyterlab/jupyterlab-github/pull/41) ([@ian-r-rose](https://github.com/ian-r-rose))
212 |
213 | ### Contributors to this release
214 |
215 | ([GitHub contributors page for this release](https://github.com/jupyterlab/jupyterlab-github/graphs/contributors?from=2018-03-23&to=2018-04-13&type=c))
216 |
217 | [@beenje](https://github.com/search?q=repo%3Ajupyterlab%2Fjupyterlab-github+involves%3Abeenje+updated%3A2018-03-23..2018-04-13&type=Issues) | [@dhirschfeld](https://github.com/search?q=repo%3Ajupyterlab%2Fjupyterlab-github+involves%3Adhirschfeld+updated%3A2018-03-23..2018-04-13&type=Issues) | [@ian-r-rose](https://github.com/search?q=repo%3Ajupyterlab%2Fjupyterlab-github+involves%3Aian-r-rose+updated%3A2018-03-23..2018-04-13&type=Issues)
218 |
219 | ## v0.5.1
220 |
221 | ([full changelog](https://github.com/jupyterlab/jupyterlab-github/compare/429c6fa...7e506c2))
222 |
223 | ### Enhancements made
224 |
225 | - Binder tree handler [#33](https://github.com/jupyterlab/jupyterlab-github/pull/33) ([@ian-r-rose](https://github.com/ian-r-rose))
226 |
227 | ### Bugs fixed
228 |
229 | - Fix decoding of base64 strings to Unicode. [#40](https://github.com/jupyterlab/jupyterlab-github/pull/40) ([@ian-r-rose](https://github.com/ian-r-rose))
230 | - Binder tree handler [#33](https://github.com/jupyterlab/jupyterlab-github/pull/33) ([@ian-r-rose](https://github.com/ian-r-rose))
231 |
232 | ### Maintenance and upkeep improvements
233 |
234 | - Update keyword [#37](https://github.com/jupyterlab/jupyterlab-github/pull/37) ([@ian-r-rose](https://github.com/ian-r-rose))
235 | - Update packaging to use new conf.d approach in Notebook 5.3 [#32](https://github.com/jupyterlab/jupyterlab-github/pull/32) ([@ian-r-rose](https://github.com/ian-r-rose))
236 | - Updates for JupyterLab 0.31rc2 [#31](https://github.com/jupyterlab/jupyterlab-github/pull/31) ([@ian-r-rose](https://github.com/ian-r-rose))
237 |
238 | ### Other merged PRs
239 |
240 | - Update README.md [#38](https://github.com/jupyterlab/jupyterlab-github/pull/38) ([@andersy005](https://github.com/andersy005))
241 | - Add package metadata for discovery compatibility [#36](https://github.com/jupyterlab/jupyterlab-github/pull/36) ([@vidartf](https://github.com/vidartf))
242 |
243 | ### Contributors to this release
244 |
245 | ([GitHub contributors page for this release](https://github.com/jupyterlab/jupyterlab-github/graphs/contributors?from=2018-01-10&to=2018-03-23&type=c))
246 |
247 | [@andersy005](https://github.com/search?q=repo%3Ajupyterlab%2Fjupyterlab-github+involves%3Aandersy005+updated%3A2018-01-10..2018-03-23&type=Issues) | [@ian-r-rose](https://github.com/search?q=repo%3Ajupyterlab%2Fjupyterlab-github+involves%3Aian-r-rose+updated%3A2018-01-10..2018-03-23&type=Issues) | [@jasongrout](https://github.com/search?q=repo%3Ajupyterlab%2Fjupyterlab-github+involves%3Ajasongrout+updated%3A2018-01-10..2018-03-23&type=Issues) | [@vidartf](https://github.com/search?q=repo%3Ajupyterlab%2Fjupyterlab-github+involves%3Avidartf+updated%3A2018-01-10..2018-03-23&type=Issues)
248 |
249 | ## v0.5.0
250 |
251 | ([full changelog](https://github.com/jupyterlab/jupyterlab-github/compare/c587338...429c6fa))
252 |
253 | ### Maintenance and upkeep improvements
254 |
255 | - Use new path parsing. [#29](https://github.com/jupyterlab/jupyterlab-github/pull/29) ([@ian-r-rose](https://github.com/ian-r-rose))
256 | - Updates for JLab 0.31 [#27](https://github.com/jupyterlab/jupyterlab-github/pull/27) ([@ian-r-rose](https://github.com/ian-r-rose))
257 | - Update services [#26](https://github.com/jupyterlab/jupyterlab-github/pull/26) ([@ian-r-rose](https://github.com/ian-r-rose))
258 |
259 | ### Other merged PRs
260 |
261 | - Fix capitalization of JupyterLab in README [#28](https://github.com/jupyterlab/jupyterlab-github/pull/28) ([@willingc](https://github.com/willingc))
262 |
263 | ### Contributors to this release
264 |
265 | ([GitHub contributors page for this release](https://github.com/jupyterlab/jupyterlab-github/graphs/contributors?from=2017-12-05&to=2018-01-10&type=c))
266 |
267 | [@ian-r-rose](https://github.com/search?q=repo%3Ajupyterlab%2Fjupyterlab-github+involves%3Aian-r-rose+updated%3A2017-12-05..2018-01-10&type=Issues) | [@willingc](https://github.com/search?q=repo%3Ajupyterlab%2Fjupyterlab-github+involves%3Awillingc+updated%3A2017-12-05..2018-01-10&type=Issues)
268 |
269 | ## v0.3.1
270 |
271 | ([full changelog](https://github.com/jupyterlab/jupyterlab-github/compare/e86e0e0...c587338))
272 |
273 | ### Bugs fixed
274 |
275 | - Fix for opening empty file. [#25](https://github.com/jupyterlab/jupyterlab-github/pull/25) ([@ian-r-rose](https://github.com/ian-r-rose))
276 |
277 | ### Other merged PRs
278 |
279 | - Updates for JupyterLab 0.30 [#24](https://github.com/jupyterlab/jupyterlab-github/pull/24) ([@ian-r-rose](https://github.com/ian-r-rose))
280 |
281 | ### Contributors to this release
282 |
283 | ([GitHub contributors page for this release](https://github.com/jupyterlab/jupyterlab-github/graphs/contributors?from=2017-12-04&to=2017-12-05&type=c))
284 |
285 | [@ian-r-rose](https://github.com/search?q=repo%3Ajupyterlab%2Fjupyterlab-github+involves%3Aian-r-rose+updated%3A2017-12-04..2017-12-05&type=Issues)
286 |
287 | ## v0.3.0
288 |
289 | ([full changelog](https://github.com/jupyterlab/jupyterlab-github/compare/dbee1d2...e86e0e0))
290 |
291 | ### Merged PRs
292 |
293 | - Updates for JupyterLab 0.30 [#24](https://github.com/jupyterlab/jupyterlab-github/pull/24) ([@ian-r-rose](https://github.com/ian-r-rose))
294 | - Handle symlinks, and show an error if the user tries to open a submodule [#21](https://github.com/jupyterlab/jupyterlab-github/pull/21) ([@ian-r-rose](https://github.com/ian-r-rose))
295 | - Org in path [#20](https://github.com/jupyterlab/jupyterlab-github/pull/20) ([@ian-r-rose](https://github.com/ian-r-rose))
296 | - Add mybinder badge [#19](https://github.com/jupyterlab/jupyterlab-github/pull/19) ([@ian-r-rose](https://github.com/ian-r-rose))
297 |
298 | ### Contributors to this release
299 |
300 | ([GitHub contributors page for this release](https://github.com/jupyterlab/jupyterlab-github/graphs/contributors?from=2017-11-10&to=2017-12-04&type=c))
301 |
302 | [@choldgraf](https://github.com/search?q=repo%3Ajupyterlab%2Fjupyterlab-github+involves%3Acholdgraf+updated%3A2017-11-10..2017-12-04&type=Issues) | [@dhirschfeld](https://github.com/search?q=repo%3Ajupyterlab%2Fjupyterlab-github+involves%3Adhirschfeld+updated%3A2017-11-10..2017-12-04&type=Issues) | [@ian-r-rose](https://github.com/search?q=repo%3Ajupyterlab%2Fjupyterlab-github+involves%3Aian-r-rose+updated%3A2017-11-10..2017-12-04&type=Issues) | [@michaelaye](https://github.com/search?q=repo%3Ajupyterlab%2Fjupyterlab-github+involves%3Amichaelaye+updated%3A2017-11-10..2017-12-04&type=Issues) | [@willingc](https://github.com/search?q=repo%3Ajupyterlab%2Fjupyterlab-github+involves%3Awillingc+updated%3A2017-11-10..2017-12-04&type=Issues)
303 |
304 | ## v0.2.0
305 |
306 | ([full changelog](https://github.com/jupyterlab/jupyterlab-github/compare/d994409...dbee1d2))
307 |
308 | ### Merged PRs
309 |
310 | - Updating to jupyterlab 0.29.x [#16](https://github.com/jupyterlab/jupyterlab-github/pull/16) ([@tkinz27](https://github.com/tkinz27))
311 | - Use smaller error panel image to cut down on bundle size. [#14](https://github.com/jupyterlab/jupyterlab-github/pull/14) ([@ian-r-rose](https://github.com/ian-r-rose))
312 | - Use base_url so that the extension works in a JupyterHub setting. [#13](https://github.com/jupyterlab/jupyterlab-github/pull/13) ([@ian-r-rose](https://github.com/ian-r-rose))
313 |
314 | ### Contributors to this release
315 |
316 | ([GitHub contributors page for this release](https://github.com/jupyterlab/jupyterlab-github/graphs/contributors?from=2017-10-25&to=2017-11-10&type=c))
317 |
318 | [@athornton](https://github.com/search?q=repo%3Ajupyterlab%2Fjupyterlab-github+involves%3Aathornton+updated%3A2017-10-25..2017-11-10&type=Issues) | [@ian-r-rose](https://github.com/search?q=repo%3Ajupyterlab%2Fjupyterlab-github+involves%3Aian-r-rose+updated%3A2017-10-25..2017-11-10&type=Issues) | [@tkinz27](https://github.com/search?q=repo%3Ajupyterlab%2Fjupyterlab-github+involves%3Atkinz27+updated%3A2017-10-25..2017-11-10&type=Issues)
319 |
320 | ## v0.1.1
321 |
322 | ([full changelog](https://github.com/jupyterlab/jupyterlab-github/compare/22d487a...d994409))
323 |
324 | ### Merged PRs
325 |
326 | - Escape URLs [#11](https://github.com/jupyterlab/jupyterlab-github/pull/11) ([@ian-r-rose](https://github.com/ian-r-rose))
327 | - Decode response body to utf-8 for python 3.5 compatibility. [#10](https://github.com/jupyterlab/jupyterlab-github/pull/10) ([@ian-r-rose](https://github.com/ian-r-rose))
328 |
329 | ### Contributors to this release
330 |
331 | ([GitHub contributors page for this release](https://github.com/jupyterlab/jupyterlab-github/graphs/contributors?from=2017-10-21&to=2017-10-25&type=c))
332 |
333 | [@ian-r-rose](https://github.com/search?q=repo%3Ajupyterlab%2Fjupyterlab-github+involves%3Aian-r-rose+updated%3A2017-10-21..2017-10-25&type=Issues)
334 |
--------------------------------------------------------------------------------
/src/contents.ts:
--------------------------------------------------------------------------------
1 | // Copyright (c) Jupyter Development Team.
2 | // Distributed under the terms of the Modified BSD License.
3 |
4 | import { Signal, ISignal } from '@lumino/signaling';
5 |
6 | import { PathExt, URLExt } from '@jupyterlab/coreutils';
7 |
8 | import { DocumentRegistry } from '@jupyterlab/docregistry';
9 |
10 | import { ObservableValue } from '@jupyterlab/observables';
11 |
12 | import { Contents, ServerConnection } from '@jupyterlab/services';
13 |
14 | import {
15 | browserApiRequest,
16 | proxiedApiRequest,
17 | GitHubRepo,
18 | GitHubContents,
19 | GitHubBlob,
20 | GitHubFileContents,
21 | GitHubDirectoryListing
22 | } from './github';
23 |
24 | import * as base64js from 'base64-js';
25 |
26 | export const DEFAULT_GITHUB_API_URL = 'https://api.github.com';
27 | export const DEFAULT_GITHUB_BASE_URL = 'https://github.com';
28 |
29 | /**
30 | * A Contents.IDrive implementation that serves as a read-only
31 | * view onto GitHub repositories.
32 | */
33 | export class GitHubDrive implements Contents.IDrive {
34 | /**
35 | * Construct a new drive object.
36 | *
37 | * @param options - The options used to initialize the object.
38 | */
39 | constructor(registry: DocumentRegistry) {
40 | this._serverSettings = ServerConnection.makeSettings();
41 | this._fileTypeForPath = (path: string) => {
42 | const types = registry.getFileTypesForPath(path);
43 | return types.length === 0 ? registry.getFileType('text')! : types[0];
44 | };
45 |
46 | this.baseUrl = DEFAULT_GITHUB_BASE_URL;
47 |
48 | // Test an api request to the notebook server
49 | // to see if the server proxy is installed.
50 | // If so, use that. If not, warn the user and
51 | // use the client-side implementation.
52 | this._useProxy = new Promise(resolve => {
53 | const requestUrl = URLExt.join(this._serverSettings.baseUrl, 'github');
54 | proxiedApiRequest(requestUrl, this._serverSettings)
55 | .then(() => {
56 | resolve(true);
57 | })
58 | .catch(() => {
59 | console.warn(
60 | 'The JupyterLab GitHub server extension appears ' +
61 | 'to be missing. If you do not install it with application ' +
62 | 'credentials, you are likely to be rate limited by GitHub ' +
63 | 'very quickly'
64 | );
65 | resolve(false);
66 | });
67 | });
68 |
69 | // Initialize the rate-limited observable.
70 | this.rateLimitedState = new ObservableValue(false);
71 | }
72 |
73 | /**
74 | * The name of the drive.
75 | */
76 | get name(): 'GitHub' {
77 | return 'GitHub';
78 | }
79 |
80 | /**
81 | * State for whether the user is valid.
82 | */
83 | get validUser(): boolean {
84 | return this._validUser;
85 | }
86 |
87 | /**
88 | * Settings for the notebook server.
89 | */
90 | get serverSettings(): ServerConnection.ISettings {
91 | return this._serverSettings;
92 | }
93 |
94 | /**
95 | * State for whether the drive is being rate limited by GitHub.
96 | */
97 | readonly rateLimitedState: ObservableValue;
98 |
99 | /**
100 | * A signal emitted when a file operation takes place.
101 | */
102 | get fileChanged(): ISignal {
103 | return this._fileChanged;
104 | }
105 |
106 | /**
107 | * Test whether the manager has been disposed.
108 | */
109 | get isDisposed(): boolean {
110 | return this._isDisposed;
111 | }
112 |
113 | /**
114 | * Dispose of the resources held by the manager.
115 | */
116 | dispose(): void {
117 | if (this.isDisposed) {
118 | return;
119 | }
120 | this._isDisposed = true;
121 | Signal.clearData(this);
122 | }
123 |
124 | /**
125 | * The GitHub base URL
126 | */
127 | get baseUrl(): string {
128 | return this._baseUrl;
129 | }
130 |
131 | /**
132 | * The GitHub base URL is set by the settingsRegistry change hook
133 | */
134 | set baseUrl(url: string) {
135 | this._baseUrl = url;
136 | }
137 |
138 | /**
139 | * The GitHub access token
140 | */
141 | get accessToken(): string | null | undefined {
142 | return this._accessToken;
143 | }
144 |
145 | /**
146 | * The GitHub access token is set by the settingsRegistry change hook
147 | */
148 | set accessToken(token: string | null | undefined) {
149 | this._accessToken = token;
150 | }
151 |
152 | /**
153 | * Get a file or directory.
154 | *
155 | * @param path: The path to the file.
156 | *
157 | * @param options: The options used to fetch the file.
158 | *
159 | * @returns A promise which resolves with the file content.
160 | */
161 | get(
162 | path: string,
163 | options?: Contents.IFetchOptions
164 | ): Promise {
165 | const resource = parsePath(path);
166 | // If the org has not been set, return an empty directory
167 | // placeholder.
168 | if (resource.user === '') {
169 | this._validUser = false;
170 | return Promise.resolve(Private.dummyDirectory);
171 | }
172 |
173 | // If the org has been set and the path is empty, list
174 | // the repositories for the org.
175 | if (resource.user && !resource.repository) {
176 | return this._listRepos(resource.user);
177 | }
178 |
179 | // Otherwise identify the repository and get the contents of the
180 | // appropriate resource.
181 | const apiPath = URLExt.encodeParts(
182 | URLExt.join(
183 | 'repos',
184 | resource.user,
185 | resource.repository,
186 | 'contents',
187 | resource.path
188 | )
189 | );
190 | return this._apiRequest(apiPath)
191 | .then(contents => {
192 | // Set the states
193 | this._validUser = true;
194 | if (this.rateLimitedState.get() !== false) {
195 | this.rateLimitedState.set(false);
196 | }
197 |
198 | return Private.gitHubContentsToJupyterContents(
199 | path,
200 | contents,
201 | this._fileTypeForPath
202 | );
203 | })
204 | .catch((err: ServerConnection.ResponseError) => {
205 | if (err.response.status === 404) {
206 | console.warn(
207 | 'GitHub: cannot find org/repo. ' +
208 | 'Perhaps you misspelled something?'
209 | );
210 | this._validUser = false;
211 | return Private.dummyDirectory;
212 | } else if (
213 | err.response.status === 403 &&
214 | err.message.indexOf('rate limit') !== -1
215 | ) {
216 | if (this.rateLimitedState.get() !== true) {
217 | this.rateLimitedState.set(true);
218 | }
219 | console.error(err.message);
220 | return Promise.reject(err);
221 | } else if (
222 | err.response.status === 403 &&
223 | err.message.indexOf('blob') !== -1
224 | ) {
225 | // Set the states
226 | this._validUser = true;
227 | if (this.rateLimitedState.get() !== false) {
228 | this.rateLimitedState.set(false);
229 | }
230 | return this._getBlob(path);
231 | } else {
232 | console.error(err.message);
233 | return Promise.reject(err);
234 | }
235 | });
236 | }
237 |
238 | /**
239 | * Get an encoded download url given a file path.
240 | *
241 | * @param path - An absolute POSIX file path on the server.
242 | *
243 | * #### Notes
244 | * It is expected that the path contains no relative paths,
245 | * use [[ContentsManager.getAbsolutePath]] to get an absolute
246 | * path if necessary.
247 | */
248 | getDownloadUrl(path: string): Promise {
249 | // Parse the path into user/repo/path
250 | const resource = parsePath(path);
251 | // Error if the user has not been set
252 | if (!resource.user) {
253 | return Promise.reject('GitHub: no active organization');
254 | }
255 |
256 | // Error if there is no path.
257 | if (!resource.path) {
258 | return Promise.reject('GitHub: No file selected');
259 | }
260 |
261 | // Otherwise identify the repository and get the url of the
262 | // appropriate resource.
263 | const dirname = PathExt.dirname(resource.path);
264 | const dirApiPath = URLExt.encodeParts(
265 | URLExt.join(
266 | 'repos',
267 | resource.user,
268 | resource.repository,
269 | 'contents',
270 | dirname
271 | )
272 | );
273 | return this._apiRequest(dirApiPath).then(
274 | dirContents => {
275 | for (const item of dirContents) {
276 | if (item.path === resource.path) {
277 | return item.download_url;
278 | }
279 | }
280 | throw Private.makeError(404, `Cannot find file at ${resource.path}`);
281 | }
282 | );
283 | }
284 |
285 | /**
286 | * Create a new untitled file or directory in the specified directory path.
287 | *
288 | * @param options: The options used to create the file.
289 | *
290 | * @returns A promise which resolves with the created file content when the
291 | * file is created.
292 | */
293 | newUntitled(options: Contents.ICreateOptions = {}): Promise {
294 | return Promise.reject('Repository is read only');
295 | }
296 |
297 | /**
298 | * Delete a file.
299 | *
300 | * @param path - The path to the file.
301 | *
302 | * @returns A promise which resolves when the file is deleted.
303 | */
304 | delete(path: string): Promise {
305 | return Promise.reject('Repository is read only');
306 | }
307 |
308 | /**
309 | * Rename a file or directory.
310 | *
311 | * @param path - The original file path.
312 | *
313 | * @param newPath - The new file path.
314 | *
315 | * @returns A promise which resolves with the new file contents model when
316 | * the file is renamed.
317 | */
318 | rename(path: string, newPath: string): Promise {
319 | return Promise.reject('Repository is read only');
320 | }
321 |
322 | /**
323 | * Save a file.
324 | *
325 | * @param path - The desired file path.
326 | *
327 | * @param options - Optional overrides to the model.
328 | *
329 | * @returns A promise which resolves with the file content model when the
330 | * file is saved.
331 | */
332 | save(
333 | path: string,
334 | options: Partial
335 | ): Promise {
336 | return Promise.reject('Repository is read only');
337 | }
338 |
339 | /**
340 | * Copy a file into a given directory.
341 | *
342 | * @param path - The original file path.
343 | *
344 | * @param toDir - The destination directory path.
345 | *
346 | * @returns A promise which resolves with the new contents model when the
347 | * file is copied.
348 | */
349 | copy(fromFile: string, toDir: string): Promise {
350 | return Promise.reject('Repository is read only');
351 | }
352 |
353 | /**
354 | * Create a checkpoint for a file.
355 | *
356 | * @param path - The path of the file.
357 | *
358 | * @returns A promise which resolves with the new checkpoint model when the
359 | * checkpoint is created.
360 | */
361 | createCheckpoint(path: string): Promise {
362 | return Promise.reject('Repository is read only');
363 | }
364 |
365 | /**
366 | * List available checkpoints for a file.
367 | *
368 | * @param path - The path of the file.
369 | *
370 | * @returns A promise which resolves with a list of checkpoint models for
371 | * the file.
372 | */
373 | listCheckpoints(path: string): Promise {
374 | return Promise.resolve([]);
375 | }
376 |
377 | /**
378 | * Restore a file to a known checkpoint state.
379 | *
380 | * @param path - The path of the file.
381 | *
382 | * @param checkpointID - The id of the checkpoint to restore.
383 | *
384 | * @returns A promise which resolves when the checkpoint is restored.
385 | */
386 | restoreCheckpoint(path: string, checkpointID: string): Promise {
387 | return Promise.reject('Repository is read only');
388 | }
389 |
390 | /**
391 | * Delete a checkpoint for a file.
392 | *
393 | * @param path - The path of the file.
394 | *
395 | * @param checkpointID - The id of the checkpoint to delete.
396 | *
397 | * @returns A promise which resolves when the checkpoint is deleted.
398 | */
399 | deleteCheckpoint(path: string, checkpointID: string): Promise {
400 | return Promise.reject('Read only');
401 | }
402 |
403 | /**
404 | * If a file is too large (> 1Mb), we need to access it over the
405 | * GitHub Git Data API.
406 | */
407 | private _getBlob(path: string): Promise {
408 | let blobData: GitHubFileContents;
409 | // Get the contents of the parent directory so that we can
410 | // get the sha of the blob.
411 | const resource = parsePath(path);
412 | const dirname = PathExt.dirname(resource.path);
413 | const dirApiPath = URLExt.encodeParts(
414 | URLExt.join(
415 | 'repos',
416 | resource.user,
417 | resource.repository,
418 | 'contents',
419 | dirname
420 | )
421 | );
422 | return this._apiRequest(dirApiPath)
423 | .then(dirContents => {
424 | for (const item of dirContents) {
425 | if (item.path === resource.path) {
426 | blobData = item as GitHubFileContents;
427 | return item.sha;
428 | }
429 | }
430 | throw Error('Cannot find sha for blob');
431 | })
432 | .then(sha => {
433 | // Once we have the sha, form the api url and make the request.
434 | const blobApiPath = URLExt.encodeParts(
435 | URLExt.join(
436 | 'repos',
437 | resource.user,
438 | resource.repository,
439 | 'git',
440 | 'blobs',
441 | sha
442 | )
443 | );
444 | return this._apiRequest(blobApiPath);
445 | })
446 | .then(blob => {
447 | // Convert the data to a Contents.IModel.
448 | blobData.content = blob.content;
449 | return Private.gitHubContentsToJupyterContents(
450 | path,
451 | blobData,
452 | this._fileTypeForPath
453 | );
454 | });
455 | }
456 |
457 | /**
458 | * List the repositories for the currently active user.
459 | */
460 | private _listRepos(user: string): Promise {
461 | // First, check if the `user` string is actually an org.
462 | // If will return with an error if not, and we can try
463 | // the user path.
464 | const apiPath = URLExt.encodeParts(URLExt.join('orgs', user, 'repos'));
465 | return this._apiRequest(apiPath)
466 | .catch(err => {
467 | // If we can't find the org, it may be a user.
468 | if (err.response.status === 404) {
469 | // Check if it is the authenticated user.
470 | return this._apiRequest('user')
471 | .then(currentUser => {
472 | let reposPath: string;
473 | // If we are looking at the currently authenticated user,
474 | // get all the repositories they own, which includes private ones.
475 | if (currentUser.login === user) {
476 | reposPath = 'user/repos?type=owner';
477 | } else {
478 | reposPath = URLExt.encodeParts(
479 | URLExt.join('users', user, 'repos')
480 | );
481 | }
482 | return this._apiRequest(reposPath);
483 | })
484 | .catch(err => {
485 | // If there is no authenticated user, return the public
486 | // users api path.
487 | if (err.response.status === 401) {
488 | const reposPath = URLExt.encodeParts(
489 | URLExt.join('users', user, 'repos')
490 | );
491 | return this._apiRequest(reposPath);
492 | }
493 | throw err;
494 | });
495 | }
496 | throw err;
497 | })
498 | .then(repos => {
499 | // Set the states
500 | this._validUser = true;
501 | if (this.rateLimitedState.get() !== false) {
502 | this.rateLimitedState.set(false);
503 | }
504 | return Private.reposToDirectory(repos);
505 | })
506 | .catch(err => {
507 | if (
508 | err.response.status === 403 &&
509 | err.message.indexOf('rate limit') !== -1
510 | ) {
511 | if (this.rateLimitedState.get() !== true) {
512 | this.rateLimitedState.set(true);
513 | }
514 | } else {
515 | console.error(err.message);
516 | console.warn(
517 | 'GitHub: cannot find user. ' + 'Perhaps you misspelled something?'
518 | );
519 | this._validUser = false;
520 | }
521 | return Private.dummyDirectory;
522 | });
523 | }
524 |
525 | /**
526 | * Determine whether to make the call via the
527 | * notebook server proxy or not.
528 | */
529 | private _apiRequest(apiPath: string): Promise {
530 | return this._useProxy.then(result => {
531 | const parts = apiPath.split('?');
532 | const path = parts[0];
533 | const query = (parts[1] || '').split('&');
534 | const params: { [key: string]: string } = {};
535 | for (const param of query) {
536 | if (param) {
537 | const [key, value] = param.split('=');
538 | params[key] = value;
539 | }
540 | }
541 | let requestUrl: string;
542 | if (result === true) {
543 | requestUrl = URLExt.join(this._serverSettings.baseUrl, 'github');
544 | // add the access token if defined
545 | if (this.accessToken) {
546 | params['access_token'] = this.accessToken;
547 | }
548 | } else {
549 | requestUrl = DEFAULT_GITHUB_API_URL;
550 | }
551 | if (path) {
552 | requestUrl = URLExt.join(requestUrl, path);
553 | }
554 | const newQuery = Object.keys(params)
555 | .map(key => `${key}=${params[key]}`)
556 | .join('&');
557 | requestUrl += '?' + newQuery;
558 | if (result === true) {
559 | return proxiedApiRequest(requestUrl, this._serverSettings);
560 | } else {
561 | return browserApiRequest(requestUrl);
562 | }
563 | });
564 | }
565 |
566 | private _baseUrl: string = 'github';
567 | private _accessToken: string | null | undefined;
568 | private _validUser = false;
569 | private _serverSettings: ServerConnection.ISettings;
570 | private _useProxy: Promise;
571 | private _fileTypeForPath: (path: string) => DocumentRegistry.IFileType;
572 | private _isDisposed = false;
573 | private _fileChanged = new Signal(this);
574 | }
575 |
576 | /**
577 | * Specification for a file in a repository.
578 | */
579 | export interface IGitHubResource {
580 | /**
581 | * The user or organization for the resource.
582 | */
583 | readonly user: string;
584 |
585 | /**
586 | * The repository in the organization/user.
587 | */
588 | readonly repository: string;
589 |
590 | /**
591 | * The path in the repository to the resource.
592 | */
593 | readonly path: string;
594 | }
595 |
596 | /**
597 | * Parse a path into a IGitHubResource.
598 | */
599 | export function parsePath(path: string): IGitHubResource {
600 | const parts = path.split('/');
601 | const user = parts.length > 0 ? parts[0] : '';
602 | const repository = parts.length > 1 ? parts[1] : '';
603 | const repoPath = parts.length > 2 ? URLExt.join(...parts.slice(2)) : '';
604 | return { user, repository, path: repoPath };
605 | }
606 |
607 | /**
608 | * Private namespace for utility functions.
609 | */
610 | namespace Private {
611 | /**
612 | * A dummy contents model indicating an invalid or
613 | * nonexistent repository.
614 | */
615 | export const dummyDirectory: Contents.IModel = {
616 | type: 'directory',
617 | path: '',
618 | name: '',
619 | format: 'json',
620 | content: [],
621 | created: '',
622 | writable: false,
623 | last_modified: '',
624 | mimetype: ''
625 | };
626 |
627 | /**
628 | * Given a JSON GitHubContents object returned by the GitHub API v3,
629 | * convert it to the Jupyter Contents.IModel.
630 | *
631 | * @param path - the path to the contents model in the repository.
632 | *
633 | * @param contents - the GitHubContents object.
634 | *
635 | * @param fileTypeForPath - a function that, given a path, returns
636 | * a DocumentRegistry.IFileType, used by JupyterLab to identify different
637 | * openers, icons, etc.
638 | *
639 | * @returns a Contents.IModel object.
640 | */
641 | export function gitHubContentsToJupyterContents(
642 | path: string,
643 | contents: GitHubContents | GitHubContents[],
644 | fileTypeForPath: (path: string) => DocumentRegistry.IFileType
645 | ): Contents.IModel {
646 | if (Array.isArray(contents)) {
647 | // If we have an array, it is a directory of GitHubContents.
648 | // Iterate over that and convert all of the items in the array/
649 | return {
650 | name: PathExt.basename(path),
651 | path: path,
652 | format: 'json',
653 | type: 'directory',
654 | writable: false,
655 | created: '',
656 | last_modified: '',
657 | mimetype: '',
658 | content: contents.map(c => {
659 | return gitHubContentsToJupyterContents(
660 | PathExt.join(path, c.name),
661 | c,
662 | fileTypeForPath
663 | );
664 | })
665 | } as Contents.IModel;
666 | } else if (contents.type === 'file' || contents.type === 'symlink') {
667 | // If it is a file or blob, convert to a file
668 | const fileType = fileTypeForPath(path);
669 | const fileContents = (contents as GitHubFileContents).content;
670 | let content: any;
671 | switch (fileType.fileFormat) {
672 | case 'text':
673 | content =
674 | fileContents !== undefined
675 | ? Private.b64DecodeUTF8(fileContents)
676 | : null;
677 | break;
678 | case 'base64':
679 | content = fileContents !== undefined ? fileContents : null;
680 | break;
681 | case 'json':
682 | content =
683 | fileContents !== undefined
684 | ? JSON.parse(Private.b64DecodeUTF8(fileContents))
685 | : null;
686 | break;
687 | default:
688 | throw new Error(`Unexpected file format: ${fileType.fileFormat}`);
689 | }
690 | return {
691 | name: PathExt.basename(path),
692 | path: path,
693 | format: fileType.fileFormat,
694 | type: 'file',
695 | created: '',
696 | writable: false,
697 | last_modified: '',
698 | mimetype: fileType.mimeTypes[0],
699 | content
700 | };
701 | } else if (contents.type === 'dir') {
702 | // If it is a directory, convert to that.
703 | return {
704 | name: PathExt.basename(path),
705 | path: path,
706 | format: 'json',
707 | type: 'directory',
708 | created: '',
709 | writable: false,
710 | last_modified: '',
711 | mimetype: '',
712 | content: null
713 | };
714 | } else if (contents.type === 'submodule') {
715 | // If it is a submodule, throw an error, since we cannot
716 | // GET submodules at the moment. NOTE: due to a bug in the GithHub
717 | // API, the `type` for submodules in a directory listing is incorrectly
718 | // reported as `file`: https://github.com/github/developer.github.com/commit/1b329b04cece9f3087faa7b1e0382317a9b93490
719 | // This means submodules will show up in the listing, but we still should not
720 | // open them.
721 | throw makeError(
722 | 400,
723 | `Cannot open "${contents.name}" because it is a submodule`
724 | );
725 | } else {
726 | throw makeError(
727 | 500,
728 | `"${contents.name}" has and unexpected type: ${contents.type}`
729 | );
730 | }
731 | }
732 |
733 | /**
734 | * Given an array of JSON GitHubRepo objects returned by the GitHub API v3,
735 | * convert it to the Jupyter Contents.IModel conforming to a directory of
736 | * those repositories.
737 | *
738 | * @param repo - the GitHubRepo object.
739 | *
740 | * @returns a Contents.IModel object.
741 | */
742 | export function reposToDirectory(repos: GitHubRepo[]): Contents.IModel {
743 | // If it is a directory, convert to that.
744 | const content: Contents.IModel[] = repos.map(repo => {
745 | return {
746 | name: repo.name,
747 | path: repo.full_name,
748 | format: 'json',
749 | type: 'directory',
750 | created: '',
751 | writable: false,
752 | last_modified: '',
753 | mimetype: '',
754 | content: null
755 | } as Contents.IModel;
756 | });
757 |
758 | return {
759 | name: '',
760 | path: '',
761 | format: 'json',
762 | type: 'directory',
763 | created: '',
764 | last_modified: '',
765 | writable: false,
766 | mimetype: '',
767 | content
768 | };
769 | }
770 |
771 | /**
772 | * Wrap an API error in a hacked-together error object
773 | * masquerading as an `ServerConnection.ResponseError`.
774 | */
775 | export function makeError(
776 | code: number,
777 | message: string
778 | ): ServerConnection.ResponseError {
779 | const response = new Response(message, {
780 | status: code,
781 | statusText: message
782 | });
783 | return new ServerConnection.ResponseError(response, message);
784 | }
785 |
786 | /**
787 | * Decoder from bytes to UTF-8.
788 | */
789 | const decoder = new TextDecoder('utf8');
790 |
791 | /**
792 | * Decode a base-64 encoded string into unicode.
793 | *
794 | * See https://developer.mozilla.org/en-US/docs/Web/API/WindowBase64/Base64_encoding_and_decoding#Solution_2_%E2%80%93_rewrite_the_DOMs_atob()_and_btoa()_using_JavaScript's_TypedArrays_and_UTF-8
795 | */
796 | export function b64DecodeUTF8(str: string): string {
797 | const bytes = base64js.toByteArray(str.replace(/\n/g, ''));
798 | return decoder.decode(bytes);
799 | }
800 | }
801 |
--------------------------------------------------------------------------------