diff --git a/.gitignore b/.gitignore index a0fcc445..7022fbc4 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,16 @@ build/ # python virtual env env/ venv/ +.cache/ +data8assets/ +.autopull_list +summer/ +test-repo/ + +.ipynb_checkpoints +docs/_build + +node_modules/ +package-lock.json + +nbgitpuller/static/dist diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index beaa2d02..00000000 --- a/.travis.yml +++ /dev/null @@ -1,6 +0,0 @@ -sudo: false -language: python -python: - - "3.6" -install: pip install tox-travis -script: tox diff --git a/MANIFEST.in b/MANIFEST.in index 0b16c9ee..100be3cf 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,5 +1,6 @@ include *.md include LICENSE +include package.json include setup.cfg recursive-include nbfetch/static * recursive-include nbfetch/templates * diff --git a/README.md b/README.md index 9154298b..79346f27 100644 --- a/README.md +++ b/README.md @@ -4,8 +4,11 @@ Fork of nbgitpuller allowing access to Hydroshare resources. ------------------- -One-way synchronization of a remote git repository to a local git repository, -with automatic conflict resolution. +`nbgitpuller` lets you distribute content in a git repository to your students +by having them click a simple link. [Automatic +merging](https://nbgitpuller.readthedocs.io/topic/automatic-merging.html) +ensures that your students are never exposed to `git` directly. It is primarily +used with a JupyterHub, but can also work on students' local computers. Pull Hydroshare resources to a local directory. diff --git a/binder/link_generator.ipynb b/binder/link_generator.ipynb deleted file mode 100644 index 71a8ab59..00000000 --- a/binder/link_generator.ipynb +++ /dev/null @@ -1,84 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Generate `nbgitpuller` links for your JupyterHub\n", - "\n", - "When users click an `nbgitpuller` link pointing to your JupyterHub,\n", - "\n", - "1. They are asked to log in to the JupyterHub if they have not already\n", - "2. The git repository referred to in the nbgitpuller link is made up to date in their home directory (keeping local changes if there are merge conflicts)\n", - "3. They are shown the specific notebook / directory referred to in the nbgitpuller link.\n", - "\n", - "This is a great way to distribute materials to students." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from ipywidgets import interact\n", - "from urllib.parse import urlunparse, urlparse, urlencode, parse_qs\n", - "from IPython.display import Markdown\n", - "\n", - "@interact\n", - "def make_nbgitpuller_link(hub_url='', repo_url='', branch='', filepath='', app=['notebook', 'lab']):\n", - " \"\"\"Generate an ipywidget form that creates an interact link.\"\"\"\n", - " \n", - " # Don't do anything if we don't have a hub_url or repo_url\n", - " if not (len(hub_url) > 0 and len(repo_url) > 0):\n", - " return\n", - " \n", - " # Parse the query to its constituent parts\n", - " \n", - " scheme, netloc, path, params, query_str, fragment = urlparse(hub_url.strip())\n", - " \n", - " # nbgitpuller takes arguments via query parameters.\n", - " # However, your hub might already require query parameters to function (for example, to pick a profile to launch in)\n", - " # So we preserve the parameters we get & amend them to include the git-pull info we want\n", - " query = parse_qs(query_str, keep_blank_values=True)\n", - " \n", - " \n", - " # Make sure the path doesn't contain multiple slashes\n", - " if not path.endswith('/'):\n", - " path += '/'\n", - " path += 'hub/user-redirect/git-pull'\n", - " \n", - " # Construct query parameters from \n", - " for name, val in [('repo', repo_url), ('branch', branch), ('subPath', filepath), ('app', app)]:\n", - " if len(val) > 0:\n", - " query[name] = val.strip()\n", - " \n", - " url = urlunparse((scheme, netloc, path, params, urlencode(query, doseq=True), fragment))\n", - " \n", - " # FIXME: Have this return something instead of print so we can unit test\n", - " print(url)" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.6.5" - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} diff --git a/binder/postBuild b/binder/postBuild deleted file mode 100644 index 82984087..00000000 --- a/binder/postBuild +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/bash -jupyter nbextension enable --py --sys-prefix appmode -jupyter serverextension enable --py --sys-prefix appmode \ No newline at end of file diff --git a/binder/requirements.txt b/binder/requirements.txt deleted file mode 100644 index 87cc5638..00000000 --- a/binder/requirements.txt +++ /dev/null @@ -1,2 +0,0 @@ -ipywidgets -appmode \ No newline at end of file diff --git a/doc/nbpuller.gif b/doc/nbpuller.gif deleted file mode 100644 index 9ccbe76d..00000000 Binary files a/doc/nbpuller.gif and /dev/null differ diff --git a/nbfetch/__init__.py b/nbfetch/__init__.py index a8d3e982..cf07e1a4 100644 --- a/nbfetch/__init__.py +++ b/nbfetch/__init__.py @@ -79,6 +79,67 @@ def initialize_handlers(self): ] ) - -def _jupyter_server_extension_paths(): - return [{"module": "nbfetch", "app": NbFetchApp}] +def _jupyter_server_extension_points(): + """ + This function is detected by `notebook` and `jupyter_server` because they + are explicitly configured to inspect the nbgitpuller module for it. That + explicit configuration is passed via setup.py's declared data_files. + + Returns a list of dictionaries with metadata describing where to find the + `_load_jupyter_server_extension` function. + """ + return [{ + "module": "nbfetch", "app": NbFetchApp, + }] + + +def _load_jupyter_server_extension(app): + """ + This function is a hook for `notebook` and `jupyter_server` that we use to + register additional endpoints to be handled by nbfetch. + + Note that as this function is used as a hook for both notebook and + jupyter_server, the argument passed may be a NotebookApp or a ServerApp. + + Related documentation: + - notebook: https://jupyter-notebook.readthedocs.io/en/stable/extending/handlers.htmland + - notebook: https://jupyter-notebook.readthedocs.io/en/stable/examples/Notebook/Distributing%20Jupyter%20Extensions%20as%20Python%20Packages.html#Example---Server-extension + - jupyter_server: https://jupyter-server.readthedocs.io/en/latest/developers/extensions.html + """ + web_app = app.web_app + base_url = url_path_join(web_app.settings['base_url'], 'nbfetch') + handlers = [ + (url_path_join(base_url, 'api'), SyncHandler), + (base_url, UIHandler), + (url_path_join(web_app.settings['base_url'], 'git-sync'), LegacyGitSyncRedirectHandler), + (url_path_join(web_app.settings['base_url'], 'interact'), LegacyInteractRedirectHandler), + ( + url_path_join(base_url, 'static', '(.*)'), + StaticFileHandler, + {'path': os.path.join(os.path.dirname(__file__), 'static')} + ) + ] + # FIXME: See note on how to stop relying on settings to pass information: + # https://github.com/jupyterhub/nbgitpuller/pull/242#pullrequestreview-854968180 + # + web_app.settings['nbapp'] = app + web_app.add_handlers('.*', handlers) + + +# For compatibility with both notebook and jupyter_server, we define +# _jupyter_server_extension_paths alongside _jupyter_server_extension_points. +# +# "..._paths" is used by notebook and still supported by jupyter_server as of +# jupyter_server 1.13.3, but was renamed to "..._points" in jupyter_server +# 1.0.0. +# +_jupyter_server_extension_paths = _jupyter_server_extension_points + +# For compatibility with both notebook and jupyter_server, we define both +# load_jupyter_server_extension alongside _load_jupyter_server_extension. +# +# "load..." is used by notebook and "_load..." is used by jupyter_server. +# + +# TODO: DRC 06.06.23 hope we can get away without this... +# load_jupyter_server_extension = _load_jupyter_server_extension diff --git a/nbfetch/pull.py b/nbfetch/pull.py index 121f5735..3d0f1ce3 100644 --- a/nbfetch/pull.py +++ b/nbfetch/pull.py @@ -15,6 +15,9 @@ def execute_cmd(cmd, **kwargs): yield '$ {}\n'.format(' '.join(cmd)) kwargs['stdout'] = subprocess.PIPE kwargs['stderr'] = subprocess.STDOUT + # Explicitly set LANG=C, as `git` commandline output will be different if + # the user environment has a different locale set! + kwargs['env'] = dict(os.environ, LANG='C') proc = subprocess.Popen(cmd, **kwargs) @@ -23,6 +26,7 @@ def execute_cmd(cmd, **kwargs): # This should behave the same as .readline(), but splits on `\r` OR `\n`, # not just `\n`. buf = [] + def flush(): line = b''.join(buf).decode('utf8', 'replace') buf[:] = [] @@ -66,8 +70,65 @@ def __init__(self, git_url, branch_name, repo_dir): assert git_url and branch_name self.git_url = git_url - self.branch_name = branch_name + self.branch_name = kwargs.pop("branch") + + if self.branch_name is None: + self.branch_name = self.resolve_default_branch() + elif not self.branch_exists(self.branch_name): + raise ValueError(f"Branch: {self.branch_name} -- not found in repo: {self.git_url}") + self.repo_dir = repo_dir + newargs = {k: v for k, v in kwargs.items() if v is not None} + super(GitPuller, self).__init__(**newargs) + + def branch_exists(self, branch): + """ + This checks to make sure the branch we are told to access + exists in the repo + """ + heads = subprocess.run( + ["git", "ls-remote", "--heads", "--", self.git_url], + capture_output=True, + text=True, + check=True + ) + tags = subprocess.run( + ["git", "ls-remote", "--tags", "--", self.git_url], + capture_output=True, + text=True, + check=True + ) + lines = heads.stdout.splitlines() + tags.stdout.splitlines() + branches = [] + for line in lines: + _, ref = line.split() + refs, heads, branch_name = ref.split("/", 2) + branches.append(branch_name) + return branch in branches + + def resolve_default_branch(self): + """ + This will resolve the default branch of the repo in + the case where the branch given does not exist + """ + try: + head_branch = subprocess.run( + ["git", "ls-remote", "--symref", "--", self.git_url, "HEAD"], + capture_output=True, + text=True, + check=True + ) + for line in head_branch.stdout.splitlines(): + if line.startswith("ref:"): + # line resembles --> ref: refs/heads/main HEAD + _, ref, head = line.split() + refs, heads, branch_name = ref.split("/", 2) + return branch_name + raise ValueError(f"default branch not found in {self.git_url}") + except subprocess.CalledProcessError: + m = f"Problem accessing HEAD branch: {self.git_url}" + logging.exception(m) + raise ValueError(m) def pull(self): """ @@ -81,17 +142,17 @@ def pull(self): def initialize_repo(self): """ - Clones repository & sets up usernames. + Clones repository """ - logging.info('Repo {} doesn\'t exist. Cloning...'.format(self.repo_dir)) - yield from execute_cmd(['git', 'clone', self.git_url, self.repo_dir]) - yield from execute_cmd(['git', 'checkout', self.branch_name], cwd=self.repo_dir) - yield from execute_cmd(['git', 'config', 'user.email', 'nbgitpuller@example.com'], cwd=self.repo_dir) - yield from execute_cmd(['git', 'config', 'user.name', 'nbgitpuller'], cwd=self.repo_dir) + clone_args = ['git', 'clone'] + if self.depth and self.depth > 0: + clone_args.extend(['--depth', str(self.depth)]) + clone_args.extend(['--branch', self.branch_name]) + clone_args.extend(["--", self.git_url, self.repo_dir]) + yield from execute_cmd(clone_args) logging.info('Repo {} initialized'.format(self.repo_dir)) - def reset_deleted_files(self): """ Runs the equivalent of git checkout -- for each file that was @@ -101,12 +162,24 @@ def reset_deleted_files(self): yield from self.ensure_lock() deleted_files = subprocess.check_output([ - 'git', 'ls-files', '--deleted' - ], cwd=self.repo_dir).decode().strip().split('\n') + 'git', 'ls-files', '--deleted', '-z' + ], cwd=self.repo_dir).decode().strip().split('\0') + upstream_deleted = self.find_upstream_changed('D') for filename in deleted_files: - if filename: # Filter out empty lines - yield from execute_cmd(['git', 'checkout', '--', filename], cwd=self.repo_dir) + if not filename: + # filter out empty lines + continue + + if filename in upstream_deleted: + # deleted in _both_, avoid conflict with git 2.40 by checking it out + # even though it's just about to be deleted + yield from execute_cmd( + ['git', 'checkout', 'HEAD', '--', filename], cwd=self.repo_dir + ) + else: + # not deleted in upstream, restore with checkout + yield from execute_cmd(['git', 'checkout', 'origin/{}'.format(self.branch_name), '--', filename], cwd=self.repo_dir) def repo_is_dirty(self): """ @@ -130,13 +203,13 @@ def find_upstream_changed(self, kind): Return list of files that have been changed upstream belonging to a particular kind of change """ output = subprocess.check_output([ - 'git', 'log', '{}..origin/{}'.format(self.branch_name, self.branch_name), - '--oneline', '--name-status' + 'git', 'diff', '..origin/{}'.format(self.branch_name), + '--name-status' ], cwd=self.repo_dir).decode() files = [] for line in output.split('\n'): if line.startswith(kind): - files.append(os.path.join(self.repo_dir, line.split('\t', 1)[1])) + files.append(line.split('\t', 1)[1]) return files @@ -171,6 +244,7 @@ def rename_local_untracked(self): # Find what files have been added! new_upstream_files = self.find_upstream_changed('A') for f in new_upstream_files: + f = os.path.join(self.repo_dir, f) if os.path.exists(f): # If there's a file extension, put the timestamp before that ts = datetime.datetime.now().strftime('__%Y%m%d%H%M%S') @@ -180,6 +254,61 @@ def rename_local_untracked(self): os.rename(f, new_file_name) yield 'Renamed {} to {} to avoid conflict with upstream'.format(f, new_file_name) + def merge(self): + """ + Merges branch from origin into current branch, resolving conflicts when possible. + + Resolves conflicts in two ways: + + - Passes `-Xours` to git, setting merge-strategy to preserve changes made by the + user whererver possible + - Detect (modify/delete) conflicts, where the user has locally modified something + that was deleted upstream. We just keep the local file. + """ + modify_delete_conflict = False + try: + for line in execute_cmd([ + 'git', + '-c', 'user.email=nbgitpuller@nbgitpuller.link', + '-c', 'user.name=nbgitpuller', + 'merge', + '-Xours', 'origin/{}'.format(self.branch_name) + ], + cwd=self.repo_dir): + yield line + # Detect conflict caused by one branch + if line.startswith("CONFLICT (modify/delete)"): + modify_delete_conflict = True + except subprocess.CalledProcessError: + if not modify_delete_conflict: + raise + + if modify_delete_conflict: + yield "Caught modify/delete conflict, trying to resolve" + # If a file was deleted on one branch, and modified on another, + # we just keep the modified file. This is done by `git add`ing it. + yield from self.commit_all() + + def commit_all(self): + """ + Creates a new commit with all current changes + """ + yield from execute_cmd([ + 'git', + # We explicitly set user info of the commits we are making, to keep that separate from + # whatever author info is set in system / repo config by the user. We pass '-c' to git + # itself (rather than to 'git commit') to temporarily set config variables. This is + # better than passing --author, since git treats author separately from committer. + '-c', 'user.email=nbgitpuller@nbgitpuller.link', + '-c', 'user.name=nbgitpuller', + 'commit', + '-am', 'Automatic commit by nbgitpuller', + # We allow empty commits. On NFS (at least), sometimes repo_is_dirty returns a false + # positive, returning True even when there are no local changes (git diff-files seems to return + # bogus output?). While ideally that would not happen, allowing empty commits keeps us + # resilient to that issue. + '--allow-empty' + ], cwd=self.repo_dir) def update(self): """ @@ -197,19 +326,19 @@ def update(self): # a fresh copy of a file they might have screwed up. yield from self.reset_deleted_files() + # Unstage any changes, otherwise the merge might fail. + # The following command resets the index, but keeps the working tree. All changes + # to files will be preserved, but they are no longer staged for commit. + yield from execute_cmd(['git', 'reset', '--mixed'], cwd=self.repo_dir) + # If there are local changes, make a commit so we can do merges when pulling - # We also allow empty commits. On NFS (at least), sometimes repo_is_dirty returns a false - # positive, returning True even when there are no local changes (git diff-files seems to return - # bogus output?). While ideally that would not happen, allowing empty commits keeps us - # resilient to that issue. if self.repo_is_dirty(): yield from self.ensure_lock() - yield from execute_cmd(['git', 'commit', '-am', 'WIP', '--allow-empty'], cwd=self.repo_dir) + yield from self.commit_all() # Merge master into local! yield from self.ensure_lock() - yield from execute_cmd(['git', 'merge', '-Xours', 'origin/{}'.format(self.branch_name)], cwd=self.repo_dir) - + yield from self.merge() def main(): @@ -222,13 +351,17 @@ def main(): parser = argparse.ArgumentParser(description='Synchronizes a github repository with a local repository.') parser.add_argument('git_url', help='Url of the repo to sync') - parser.add_argument('branch_name', default='master', help='Branch of repo to sync', nargs='?') + parser.add_argument('branch_name', default=None, help='Branch of repo to sync', nargs='?') parser.add_argument('repo_dir', default='.', help='Path to clone repo under', nargs='?') args = parser.parse_args() for line in GitPuller( args.git_url, - args.branch_name, - args.repo_dir + args.repo_dir, + branch=args.branch_name if args.branch_name else None ).pull(): print(line) + + +if __name__ == '__main__': + main() diff --git a/nbfetch/static/index.js b/nbfetch/static/index.js deleted file mode 100644 index 53b43c42..00000000 --- a/nbfetch/static/index.js +++ /dev/null @@ -1,278 +0,0 @@ -require([ - 'jquery', - 'base/js/utils', - 'components/xterm.js/index', - 'components/xterm.js-fit/index' -], function( - $, - utils, - Terminal, - fit -) { - - Terminal.applyAddon(fit); - - function GitSync(baseUrl, repo, branch, path) { - // Class that talks to the API backend & emits events as appropriate - this.baseUrl = baseUrl; - this.repo = repo; - this.branch = branch; - this.redirectUrl = baseUrl + path; - - this.callbacks = {}; - } - - GitSync.prototype.addHandler = function(event, cb) { - if (this.callbacks[event] == undefined) { - this.callbacks[event] = [cb]; - } else { - this.callbacks[event].push(cb); - } - }; - - GitSync.prototype._emit = function(event, data) { - if (this.callbacks[event] == undefined) { return; } - $.each(this.callbacks[event], function(i, ev) { - ev(data); - }); - }; - - - GitSync.prototype.start = function() { - // Start git pulling handled by SyncHandler, declared in handlers.py - var syncUrl = this.baseUrl + 'git-pull/api?' + $.param({ - repo: this.repo, - branch: this.branch - }); - - this.eventSource = new EventSource(syncUrl); - var that = this; - this.eventSource.addEventListener('message', function(ev) { - var data = JSON.parse(ev.data); - if (data.phase == 'finished' || data.phase == 'error') { - that.eventSource.close(); - } - that._emit(data.phase, data); - }); - this.eventSource.addEventListener('error', function(error) { - console.log(arguments); - that._emit('error', error); - }); - }; - - function GitSyncView(termSelector, progressSelector, termToggleSelector) { - // Class that encapsulates view rendering as much as possible - this.term = new Terminal({ - convertEol: true - }); - this.visible = false; - this.$progress = $(progressSelector); - - this.$termToggle = $(termToggleSelector); - this.termSelector = termSelector; - - var that = this; - this.$termToggle.click(function() { - that.setTerminalVisibility(!that.visible); - }); - } - - GitSyncView.prototype.setTerminalVisibility = function(visible) { - if (visible) { - $(this.termSelector).parent().removeClass('hidden'); - } else { - $(this.termSelector).parent().addClass('hidden'); - } - this.visible = visible; - if (visible) { - // See https://github.com/data-8/nbgitpuller/pull/46 on why this is here. - if (!this.term.element) { - this.term.open($(this.termSelector)[0]); - } - this.term.fit(); - } - - } - - GitSyncView.prototype.setProgressValue = function(val) { - this.$progress.attr('aria-valuenow', val); - this.$progress.css('width', val + '%'); - }; - - GitSyncView.prototype.getProgressValue = function() { - return parseFloat(this.$progress.attr('aria-valuenow')); - }; - - GitSyncView.prototype.setProgressText = function(text) { - this.$progress.children('span').text(text); - }; - - GitSyncView.prototype.getProgressText = function() { - return this.$progress.children('span').text(); - }; - - GitSyncView.prototype.setProgressError = function(isError) { - if (isError) { - this.$progress.addClass('progress-bar-danger'); - } else { - this.$progress.removeClass('progress-bar-danger'); - } - }; - - var gs = new GitSync( - utils.get_body_data('baseUrl'), - utils.get_body_data('repo'), - utils.get_body_data('branch'), - utils.get_body_data('path') - ); - - var gsv = new GitSyncView( - '#status-details', - '#status-panel-title', - '#status-panel-toggle' - ); - - gs.addHandler('syncing', function(data) { - gsv.term.write(data.output); - }); - gs.addHandler('finished', function(data) { - progressTimers.forEach(function(timer) { clearInterval(timer); }); - gsv.setProgressValue(100); - gsv.setProgressText('Sync finished, redirecting...'); - window.location.href = gs.redirectUrl; - }); - gs.addHandler('error', function(data) { - progressTimers.forEach(function(timer) { clearInterval(timer); }); - gsv.setProgressValue(100); - gsv.setProgressText('Error: ' + data.message); - gsv.setProgressError(true); - gsv.setTerminalVisibility(true); - if (data.output) { - gsv.term.write(data.output); - } - }); - gs.start(); - - $('#header, #site').show(); - - // Make sure we provide plenty of appearances of progress! - var progressTimers = []; - progressTimers.push(setInterval(function() { - gsv.setProgressText(substatus_messages[Math.floor(Math.random() * substatus_messages.length)]); - }, 3000)); - progressTimers.push(setInterval(function() { - gsv.setProgressText(gsv.getProgressText() + '.'); - }, 800)); - - progressTimers.push(setInterval(function() { - // Illusion of progress! - gsv.setProgressValue(gsv.getProgressValue() + (0.01 * (100 - gsv.getProgressValue()))); - }, 900)); - - - var substatus_messages = [ - "Adding Hidden Agendas", - "Adjusting Bell Curves", - "Aesthesizing Industrial Areas", - "Aligning Covariance Matrices", - "Applying Feng Shui Shaders", - "Applying Theatre Soda Layer", - "Asserting Packed Exemplars", - "Attempting to Lock Back-Buffer", - "Binding Sapling Root System", - "Breeding Fauna", - "Building Data Trees", - "Bureacritizing Bureaucracies", - "Calculating Inverse Probability Matrices", - "Calculating Llama Expectoration Trajectory", - "Calibrating Blue Skies", - "Charging Ozone Layer", - "Coalescing Cloud Formations", - "Cohorting Exemplars", - "Collecting Meteor Particles", - "Compounding Inert Tessellations", - "Compressing Fish Files", - "Computing Optimal Bin Packing", - "Concatenating Sub-Contractors", - "Containing Existential Buffer", - "Debarking Ark Ramp", - "Debunching Unionized Commercial Services", - "Deciding What Message to Display Next", - "Decomposing Singular Values", - "Decrementing Tectonic Plates", - "Deleting Ferry Routes", - "Depixelating Inner Mountain Surface Back Faces", - "Depositing Slush Funds", - "Destabilizing Economic Indicators", - "Determining Width of Blast Fronts", - "Dicing Models", - "Diluting Livestock Nutrition Variables", - "Downloading Satellite Terrain Data", - "Eating Ice Cream", - "Exposing Flash Variables to Streak System", - "Extracting Resources", - "Factoring Pay Scale", - "Fixing Election Outcome Matrix", - "Flood-Filling Ground Water", - "Flushing Pipe Network", - "Gathering Particle Sources", - "Generating Jobs", - "Gesticulating Mimes", - "Graphing Whale Migration", - "Hiding Willio Webnet Mask", - "Implementing Impeachment Routine", - "Increasing Accuracy of RCI Simulators", - "Increasing Magmafacation", - "Initializing Rhinoceros Breeding Timetable", - "Initializing Robotic Click-Path AI", - "Inserting Sublimated Messages", - "Integrating Curves", - "Integrating Illumination Form Factors", - "Integrating Population Graphs", - "Iterating Cellular Automata", - "Lecturing Errant Subsystems", - "Modeling Object Components", - "Normalizing Power", - "Obfuscating Quigley Matrix", - "Overconstraining Dirty Industry Calculations", - "Partitioning City Grid Singularities", - "Perturbing Matrices", - "Polishing Water Highlights", - "Populating Lot Templates", - "Preparing Sprites for Random Walks", - "Prioritizing Landmarks", - "Projecting Law Enforcement Pastry Intake", - "Realigning Alternate Time Frames", - "Reconfiguring User Mental Processes", - "Relaxing Splines", - "Removing Road Network Speed Bumps", - "Removing Texture Gradients", - "Removing Vehicle Avoidance Behavior", - "Resolving GUID Conflict", - "Reticulating Splines", - "Retracting Phong Shader", - "Retrieving from Back Store", - "Reverse Engineering Image Consultant", - "Routing Neural Network Infanstructure", - "Scattering Rhino Food Sources", - "Scrubbing Terrain", - "Searching for Llamas", - "Seeding Architecture Simulation Parameters", - "Sequencing Particles", - "Setting Advisor Moods", - "Setting Inner Deity Indicators", - "Setting Universal Physical Constants", - "Smashing The Patriarchy", - "Sonically Enhancing Occupant-Free Timber", - "Speculating Stock Market Indices", - "Splatting Transforms", - "Stratifying Ground Layers", - "Sub-Sampling Water Data", - "Synthesizing Gravity", - "Synthesizing Wavelets", - "Time-Compressing Simulator Clock", - "Unable to Reveal Current Activity", - "Weathering Buildings", - "Zeroing Crime Network" - ]; -}); diff --git a/nbfetch/templates/status.html b/nbfetch/templates/status.html index 45cc5c4a..2b969f59 100644 --- a/nbfetch/templates/status.html +++ b/nbfetch/templates/status.html @@ -5,7 +5,9 @@ data-base-url="{{ base_url | urlencode }}" data-repo="{{ repo | urlencode }}" data-path="{{ path | urlencode }}" -data-branch="{{ branch | urlencode }}" +{% if branch %}data-branch="{{ branch | urlencode }}"{% endif %} +{% if depth %}data-depth="{{ depth | urlencode }}"{% endif %} +data-targetpath="{{ targetpath | urlencode }}" {% endblock %} {% block site %} @@ -32,15 +34,12 @@ {% block script %} {{super()}} - + {% endblock %} {% block stylesheet %} {{super()}} - -