nbgitpuller 1.2.2__tar.gz → 1.3.0b1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. {nbgitpuller-1.2.2 → nbgitpuller-1.3.0b1}/CHANGELOG.md +37 -0
  2. {nbgitpuller-1.2.2/nbgitpuller.egg-info → nbgitpuller-1.3.0b1}/PKG-INFO +3 -2
  3. {nbgitpuller-1.2.2 → nbgitpuller-1.3.0b1}/nbgitpuller/__init__.py +2 -2
  4. nbgitpuller-1.3.0b1/nbgitpuller/errors.py +117 -0
  5. {nbgitpuller-1.2.2 → nbgitpuller-1.3.0b1}/nbgitpuller/handlers.py +29 -15
  6. {nbgitpuller-1.2.2 → nbgitpuller-1.3.0b1}/nbgitpuller/pull.py +40 -10
  7. nbgitpuller-1.3.0b1/nbgitpuller/static/dist/bundle.js +3 -0
  8. nbgitpuller-1.3.0b1/nbgitpuller/static/dist/bundle.js.LICENSE.txt +1 -0
  9. nbgitpuller-1.3.0b1/nbgitpuller/static/dist/bundle.js.map +1 -0
  10. nbgitpuller-1.3.0b1/nbgitpuller/static/js/giterror.js +61 -0
  11. {nbgitpuller-1.2.2 → nbgitpuller-1.3.0b1}/nbgitpuller/static/js/gitsync.js +5 -1
  12. {nbgitpuller-1.2.2 → nbgitpuller-1.3.0b1}/nbgitpuller/static/js/gitsyncview.js +25 -1
  13. {nbgitpuller-1.2.2 → nbgitpuller-1.3.0b1}/nbgitpuller/static/js/index.js +16 -5
  14. {nbgitpuller-1.2.2 → nbgitpuller-1.3.0b1}/nbgitpuller/templates/status.html +28 -3
  15. {nbgitpuller-1.2.2 → nbgitpuller-1.3.0b1}/nbgitpuller/version.py +1 -1
  16. {nbgitpuller-1.2.2 → nbgitpuller-1.3.0b1/nbgitpuller.egg-info}/PKG-INFO +3 -2
  17. {nbgitpuller-1.2.2 → nbgitpuller-1.3.0b1}/nbgitpuller.egg-info/SOURCES.txt +3 -0
  18. {nbgitpuller-1.2.2 → nbgitpuller-1.3.0b1}/package.json +1 -0
  19. {nbgitpuller-1.2.2 → nbgitpuller-1.3.0b1}/pyproject.toml +1 -1
  20. {nbgitpuller-1.2.2 → nbgitpuller-1.3.0b1}/setup.py +1 -1
  21. {nbgitpuller-1.2.2 → nbgitpuller-1.3.0b1}/tests/test_api.py +15 -0
  22. {nbgitpuller-1.2.2 → nbgitpuller-1.3.0b1}/tests/test_gitpuller.py +69 -4
  23. nbgitpuller-1.2.2/nbgitpuller/static/dist/bundle.js +0 -2
  24. nbgitpuller-1.2.2/nbgitpuller/static/dist/bundle.js.map +0 -1
  25. {nbgitpuller-1.2.2 → nbgitpuller-1.3.0b1}/LICENSE +0 -0
  26. {nbgitpuller-1.2.2 → nbgitpuller-1.3.0b1}/MANIFEST.in +0 -0
  27. {nbgitpuller-1.2.2 → nbgitpuller-1.3.0b1}/README.md +0 -0
  28. {nbgitpuller-1.2.2 → nbgitpuller-1.3.0b1}/RELEASE.md +0 -0
  29. {nbgitpuller-1.2.2 → nbgitpuller-1.3.0b1}/nbgitpuller/_compat.py +0 -0
  30. {nbgitpuller-1.2.2 → nbgitpuller-1.3.0b1}/nbgitpuller/etc/jupyter_notebook_config.d/nbgitpuller.json +0 -0
  31. {nbgitpuller-1.2.2 → nbgitpuller-1.3.0b1}/nbgitpuller/etc/jupyter_server_config.d/nbgitpuller.json +0 -0
  32. {nbgitpuller-1.2.2 → nbgitpuller-1.3.0b1}/nbgitpuller/templates/page.html +0 -0
  33. {nbgitpuller-1.2.2 → nbgitpuller-1.3.0b1}/nbgitpuller.egg-info/dependency_links.txt +0 -0
  34. {nbgitpuller-1.2.2 → nbgitpuller-1.3.0b1}/nbgitpuller.egg-info/entry_points.txt +0 -0
  35. {nbgitpuller-1.2.2 → nbgitpuller-1.3.0b1}/nbgitpuller.egg-info/not-zip-safe +0 -0
  36. {nbgitpuller-1.2.2 → nbgitpuller-1.3.0b1}/nbgitpuller.egg-info/requires.txt +0 -0
  37. {nbgitpuller-1.2.2 → nbgitpuller-1.3.0b1}/nbgitpuller.egg-info/top_level.txt +0 -0
  38. {nbgitpuller-1.2.2 → nbgitpuller-1.3.0b1}/setup.cfg +0 -0
@@ -1,3 +1,40 @@
1
+ ## 1.3
2
+
3
+ ### 1.3.0b1 (beta release) - 2026-02-04
4
+
5
+ ([full changelog](https://github.com/jupyterhub/nbgitpuller/compare/1.2.2...7af8b80))
6
+
7
+ #### New features added
8
+
9
+ - Add `copy error to clipboard` button [#382](https://github.com/jupyterhub/nbgitpuller/pull/382) ([@jnywong](https://github.com/jnywong), [@balajialg](https://github.com/balajialg), [@yuvipanda](https://github.com/yuvipanda))
10
+ - Add link to go back to root when an error is thrown [#380](https://github.com/jupyterhub/nbgitpuller/pull/380) ([@cmarmo](https://github.com/cmarmo), [@jnywong](https://github.com/jnywong), [@nthiery](https://github.com/nthiery), [@yuvipanda](https://github.com/yuvipanda))
11
+
12
+ #### Documentation improvements
13
+
14
+ - Add targetPath to link generator. [#368](https://github.com/jupyterhub/nbgitpuller/pull/368) ([@ryanlovett](https://github.com/ryanlovett), [@consideRatio](https://github.com/consideRatio))
15
+ - docs: Add warning about private tokens [#387](https://github.com/jupyterhub/nbgitpuller/pull/387) ([@jnywong](https://github.com/jnywong), [@ryanlovett](https://github.com/ryanlovett), [@yuvipanda](https://github.com/yuvipanda))
16
+ - Update reference to link.rst [#363](https://github.com/jupyterhub/nbgitpuller/pull/363) ([@jnywong](https://github.com/jnywong), [@GeorgianaElena](https://github.com/GeorgianaElena))
17
+
18
+ #### Continuous integration improvements
19
+
20
+ - Bump actions/checkout from 5 to 6 [#381](https://github.com/jupyterhub/nbgitpuller/pull/381) ([@consideRatio](https://github.com/consideRatio))
21
+ - Bump actions/setup-node from 5 to 6 [#377](https://github.com/jupyterhub/nbgitpuller/pull/377) ([@manics](https://github.com/manics))
22
+ - Bump actions/setup-python from 5 to 6 [#376](https://github.com/jupyterhub/nbgitpuller/pull/376) ([@consideRatio](https://github.com/consideRatio))
23
+ - Bump actions/checkout from 4 to 5 [#375](https://github.com/jupyterhub/nbgitpuller/pull/375) ([@consideRatio](https://github.com/consideRatio))
24
+ - Bump actions/setup-node from 4 to 5 [#374](https://github.com/jupyterhub/nbgitpuller/pull/374) ([@consideRatio](https://github.com/consideRatio))
25
+ - Support building on mybinder.org to ease testing [#258](https://github.com/jupyterhub/nbgitpuller/pull/258) ([@manics](https://github.com/manics), [@consideRatio](https://github.com/consideRatio), [@minrk](https://github.com/minrk))
26
+ - [pre-commit.ci] pre-commit autoupdate [#371](https://github.com/jupyterhub/nbgitpuller/pull/371) ([@consideRatio](https://github.com/consideRatio))
27
+ - [pre-commit.ci] pre-commit autoupdate [#366](https://github.com/jupyterhub/nbgitpuller/pull/366) ([@yuvipanda](https://github.com/yuvipanda))
28
+
29
+ #### Contributors to this release
30
+
31
+ The following people contributed discussions, new ideas, code and documentation contributions, and review.
32
+ See [our definition of contributors](https://github-activity.readthedocs.io/en/latest/use/#how-does-this-tool-define-contributions-in-the-reports).
33
+
34
+ ([GitHub contributors page for this release](https://github.com/jupyterhub/nbgitpuller/graphs/contributors?from=2025-01-27&to=2026-02-03&type=c))
35
+
36
+ @balajialg ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fnbgitpuller+involves%3Abalajialg+updated%3A2025-01-27..2026-02-03&type=Issues)) | @cmarmo ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fnbgitpuller+involves%3Acmarmo+updated%3A2025-01-27..2026-02-03&type=Issues)) | @consideRatio ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fnbgitpuller+involves%3AconsideRatio+updated%3A2025-01-27..2026-02-03&type=Issues)) | @GeorgianaElena ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fnbgitpuller+involves%3AGeorgianaElena+updated%3A2025-01-27..2026-02-03&type=Issues)) | @jacobtomlinson ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fnbgitpuller+involves%3Ajacobtomlinson+updated%3A2025-01-27..2026-02-03&type=Issues)) | @jnywong ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fnbgitpuller+involves%3Ajnywong+updated%3A2025-01-27..2026-02-03&type=Issues)) | @manics ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fnbgitpuller+involves%3Amanics+updated%3A2025-01-27..2026-02-03&type=Issues)) | @minrk ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fnbgitpuller+involves%3Aminrk+updated%3A2025-01-27..2026-02-03&type=Issues)) | @nthiery ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fnbgitpuller+involves%3Anthiery+updated%3A2025-01-27..2026-02-03&type=Issues)) | @ryanlovett ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fnbgitpuller+involves%3Aryanlovett+updated%3A2025-01-27..2026-02-03&type=Issues)) | @yuvipanda ([activity](https://github.com/search?q=repo%3Ajupyterhub%2Fnbgitpuller+involves%3Ayuvipanda+updated%3A2025-01-27..2026-02-03&type=Issues))
37
+
1
38
  ## 1.2
2
39
 
3
40
  ### 1.2.2 - 2025-01-27
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.2
1
+ Metadata-Version: 2.4
2
2
  Name: nbgitpuller
3
- Version: 1.2.2
3
+ Version: 1.3.0b1
4
4
  Summary: Jupyter Extension to do one-way synchronization of git repositories
5
5
  Home-page: https://github.com/jupyterhub/nbgitpuller
6
6
  Author: Peter Veerman, YuviPanda
@@ -25,6 +25,7 @@ Dynamic: description
25
25
  Dynamic: description-content-type
26
26
  Dynamic: home-page
27
27
  Dynamic: license
28
+ Dynamic: license-file
28
29
  Dynamic: platform
29
30
  Dynamic: requires-dist
30
31
  Dynamic: summary
@@ -1,5 +1,5 @@
1
- from .version import __version__ # noqa
2
- from .pull import GitPuller # noqa
1
+ from nbgitpuller.version import __version__ # noqa
2
+ from nbgitpuller.pull import GitPuller # noqa
3
3
  from jupyter_server.utils import url_path_join
4
4
  from tornado.web import StaticFileHandler
5
5
  import os
@@ -0,0 +1,117 @@
1
+ import subprocess
2
+ import traceback
3
+
4
+
5
+ class GitPullerError(Exception):
6
+ """
7
+ Structured error class used to branch frontend UI content based on its attributes.
8
+
9
+ Attributes:
10
+ code (str): Stable internal error code identifying the failure
11
+ type (e.g. ``"merge"``, ``"clone"``). This value is used by the
12
+ frontend to branch context specific UI behaviour.
13
+ message (str): External error message. This value is
14
+ safe to display in the frontend.
15
+ traceback (Optional[str]): Formatted traceback information captured from
16
+ the original exception for logging.
17
+ """
18
+ def __init__(self, code: str = "unknown", message: str = "Unexpected error occurred", traceback_message=None):
19
+ super().__init__(message)
20
+ self.code = code
21
+ self.message = message
22
+ self.traceback = traceback_message
23
+
24
+
25
+ def __str__(self):
26
+ return self.message
27
+
28
+
29
+ def to_dict(self):
30
+ return {
31
+ "code": self.code,
32
+ "message": self.message,
33
+ "traceback": self.traceback
34
+ }
35
+
36
+ @staticmethod
37
+ def format_traceback(exc):
38
+ tb = getattr(exc, "__traceback__", None)
39
+ traceback_message = ''.join(traceback.format_exception(type(exc), exc, tb)) if tb else None
40
+ return traceback_message
41
+
42
+
43
+ @classmethod
44
+ def from_exception(cls, exc: Exception) -> "GitPullerError":
45
+ """
46
+ Convert exceptions into a structured GitPullerError class.
47
+ """
48
+ if isinstance(exc, GitPullerError):
49
+ return exc
50
+ traceback_message = cls.format_traceback(exc)
51
+ if isinstance(exc, subprocess.CalledProcessError):
52
+ # Categorise errors based on specific git commands
53
+ if exc.cmd[1] == "clone":
54
+ return CloneError(traceback_message)
55
+ elif len(exc.cmd) >5 and exc.cmd[5] == "merge":
56
+ return MergeError(traceback_message)
57
+ elif exc.cmd[1] == "ls-remote":
58
+ return RemoteError(traceback_message)
59
+ return cls(code="unknown", message=str(exc), traceback_message=traceback_message)
60
+
61
+
62
+
63
+ class CloneError(GitPullerError):
64
+ code="clone"
65
+ message="Clone error detected"
66
+
67
+ def __init__(self, traceback_message=None):
68
+ super().__init__(
69
+ code = self.code,
70
+ message= self.message,
71
+ traceback_message=traceback_message
72
+ )
73
+
74
+
75
+ class MergeError(GitPullerError):
76
+ code="merge"
77
+ message="Merge error detected"
78
+
79
+ def __init__(self, traceback_message=None):
80
+ super().__init__(
81
+ code = self.code,
82
+ message= self.message,
83
+ traceback_message=traceback_message
84
+ )
85
+
86
+ class BranchExistError(GitPullerError):
87
+ code="branch_exist"
88
+ message="Branch does not exist"
89
+
90
+ def __init__(self, traceback_message=None):
91
+ super().__init__(
92
+ code = self.code,
93
+ message= self.message,
94
+ traceback_message=traceback_message
95
+ )
96
+
97
+ class BranchResolveError(GitPullerError):
98
+ code="branch_resolve"
99
+ message="Branch name unresolved"
100
+
101
+ def __init__(self, traceback_message=None):
102
+ super().__init__(
103
+ code = self.code,
104
+ message= self.message,
105
+ traceback_message=traceback_message
106
+ )
107
+
108
+ class RemoteError(GitPullerError):
109
+ code="ls_remote"
110
+ message="Remote content unavailable"
111
+
112
+ def __init__(self, traceback_message=None):
113
+ super().__init__(
114
+ code = self.code,
115
+ message= self.message,
116
+ traceback_message=traceback_message
117
+ )
@@ -8,9 +8,10 @@ import os
8
8
  from queue import Queue, Empty
9
9
  import jinja2
10
10
 
11
- from .pull import GitPuller
12
- from .version import __version__
13
- from ._compat import get_base_handler
11
+ from nbgitpuller.pull import GitPuller
12
+ from nbgitpuller.errors import GitPullerError
13
+ from nbgitpuller.version import __version__
14
+ from nbgitpuller._compat import get_base_handler
14
15
 
15
16
  JupyterHandler = get_base_handler()
16
17
 
@@ -57,7 +58,7 @@ class SyncHandler(JupyterHandler):
57
58
  except gen.TimeoutError:
58
59
  await self.emit({
59
60
  'phase': 'error',
60
- 'message': 'Another git operations is currently running, try again in a few minutes'
61
+ 'message': 'Another git operation is currently running, try again in a few minutes'
61
62
  })
62
63
  return
63
64
 
@@ -65,6 +66,7 @@ class SyncHandler(JupyterHandler):
65
66
  repo = self.get_argument('repo')
66
67
  branch = self.get_argument('branch', None)
67
68
  depth = self.get_argument('depth', None)
69
+ backup = self.get_argument('backup', False)
68
70
  if depth:
69
71
  depth = int(depth)
70
72
  # The default working directory is the directory from which Jupyter
@@ -84,7 +86,20 @@ class SyncHandler(JupyterHandler):
84
86
  self.set_header('content-type', 'text/event-stream')
85
87
  self.set_header('cache-control', 'no-cache')
86
88
 
87
- gp = GitPuller(repo, repo_dir, branch=branch, depth=depth, parent=self.settings['nbapp'])
89
+ try:
90
+ gp = GitPuller(repo, repo_dir, branch=branch, depth=depth, backup=backup, parent=self.settings['nbapp'])
91
+ except Exception as e:
92
+ err = GitPullerError.from_exception(e)
93
+ err.traceback = GitPullerError.format_traceback(e)
94
+ err_out = err.to_dict()
95
+ await self.emit({
96
+ 'phase': 'error',
97
+ 'message': err_out["message"],
98
+ 'error': err_out,
99
+ 'output': err_out["traceback"],
100
+ })
101
+ raise err
102
+
88
103
 
89
104
  q = Queue()
90
105
 
@@ -110,17 +125,15 @@ class SyncHandler(JupyterHandler):
110
125
  if progress is None:
111
126
  break
112
127
  if isinstance(progress, Exception):
128
+ e = GitPullerError.from_exception(progress)
129
+ err = e.to_dict()
113
130
  await self.emit({
114
131
  'phase': 'error',
115
132
  'message': str(progress),
116
- 'output': '\n'.join([
117
- line.strip()
118
- for line in traceback.format_exception(
119
- type(progress), progress, progress.__traceback__
120
- )
121
- ])
133
+ 'error': err,
134
+ 'output': err["traceback"]
122
135
  })
123
- return
136
+ raise e
124
137
 
125
138
  await self.emit({'output': progress, 'phase': 'syncing'})
126
139
 
@@ -159,11 +172,12 @@ class UIHandler(JupyterHandler):
159
172
  # working directory) and we end up with weird failures
160
173
  targetpath = self.get_argument('targetpath', None) or \
161
174
  self.get_argument('targetPath', repo.rstrip('/').split('/')[-1])
175
+ backup = self.get_argument('backup', False)
162
176
 
163
177
  if urlPath:
164
- path = urlPath
178
+ path = urlPath if not backup else os.path.join(parent_reldir)
165
179
  else:
166
- path = os.path.join(parent_reldir, targetpath, subPath)
180
+ path = os.path.join(parent_reldir, targetpath, subPath) if not backup else os.path.join(parent_reldir)
167
181
  if app.lower() == 'lab':
168
182
  path = 'lab/tree/' + path
169
183
  elif path.lower().endswith('.ipynb'):
@@ -173,7 +187,7 @@ class UIHandler(JupyterHandler):
173
187
 
174
188
  self.write(
175
189
  jinja_env.get_template('status.html').render(
176
- repo=repo, branch=branch, path=path, depth=depth, targetpath=targetpath, version=__version__,
190
+ repo=repo, branch=branch, path=path, depth=depth, targetpath=targetpath, backup=backup, version=__version__,
177
191
  **self.template_namespace
178
192
  )
179
193
  )
@@ -4,9 +4,11 @@ import logging
4
4
  import time
5
5
  import argparse
6
6
  import datetime
7
+ from itertools import count
7
8
  from traitlets import Integer, default
8
9
  from traitlets.config import Configurable
9
10
  from functools import partial
11
+ from nbgitpuller.errors import BranchExistError, BranchResolveError
10
12
 
11
13
 
12
14
  def execute_cmd(cmd, **kwargs):
@@ -73,17 +75,22 @@ class GitPuller(Configurable):
73
75
  assert git_url
74
76
 
75
77
  self.git_url = git_url
76
- self.branch_name = kwargs.pop("branch")
78
+ self.branch_name = kwargs.pop("branch", None)
79
+ self.repo_dir = repo_dir
80
+ backup = kwargs.pop("backup", False)
77
81
 
78
82
  if self.branch_name is None:
79
83
  self.branch_name = self.resolve_default_branch()
80
- elif not self.branch_exists(self.branch_name):
81
- raise ValueError(f"Branch: {self.branch_name} -- not found in repo: {self.git_url}")
84
+ else:
85
+ self.branch_exists(self.branch_name)
86
+
87
+ if backup and os.path.exists(self.repo_dir):
88
+ self.backup_repo_dir()
82
89
 
83
- self.repo_dir = repo_dir
84
90
  newargs = {k: v for k, v in kwargs.items() if v is not None}
85
91
  super(GitPuller, self).__init__(**newargs)
86
92
 
93
+
87
94
  def branch_exists(self, branch):
88
95
  """
89
96
  This checks to make sure the branch we are told to access
@@ -107,7 +114,11 @@ class GitPuller(Configurable):
107
114
  _, ref = line.split()
108
115
  refs, heads, branch_name = ref.split("/", 2)
109
116
  branches.append(branch_name)
110
- return branch in branches
117
+ if branch in branches:
118
+ return
119
+ else:
120
+ raise BranchExistError()
121
+
111
122
 
112
123
  def resolve_default_branch(self):
113
124
  """
@@ -127,11 +138,28 @@ class GitPuller(Configurable):
127
138
  _, ref, head = line.split()
128
139
  refs, heads, branch_name = ref.split("/", 2)
129
140
  return branch_name
130
- raise ValueError(f"default branch not found in {self.git_url}")
141
+ raise BranchResolveError()
131
142
  except subprocess.CalledProcessError:
132
- m = f"Problem accessing HEAD branch: {self.git_url}"
133
- logging.exception(m)
134
- raise ValueError(m)
143
+ error = BranchResolveError()
144
+ logging.exception(error)
145
+ raise error
146
+
147
+ def backup_repo_dir(self):
148
+ """
149
+ Backup the existing repo_dir if URL parameter backup=true.
150
+ """
151
+ timestamp = datetime.datetime.now().strftime('%Y%m%d%H%M%S')
152
+ backup_dir = f"{self.repo_dir}_backup_{timestamp}"
153
+ if not os.path.exists(backup_dir):
154
+ os.rename(self.repo_dir, backup_dir)
155
+ else:
156
+ # Never clobber backup_dir if it somehow exists
157
+ base_dir = backup_dir
158
+ candidate = (f"{base_dir}.latest_{i}" for i in count())
159
+ while os.path.exists(backup_dir):
160
+ backup_dir = next(candidate)
161
+ os.rename(self.repo_dir, backup_dir)
162
+ logging.info('Backed up folder {}'.format(backup_dir))
135
163
 
136
164
  def pull(self):
137
165
  """
@@ -356,12 +384,14 @@ def main():
356
384
  parser.add_argument('git_url', help='Url of the repo to sync')
357
385
  parser.add_argument('branch_name', default=None, help='Branch of repo to sync', nargs='?')
358
386
  parser.add_argument('repo_dir', default='.', help='Path to clone repo under', nargs='?')
387
+ parser.add_argument('--backup', action='store_true', default=False, help='Whether to backup existing repo_dir if it exists')
359
388
  args = parser.parse_args()
360
389
 
361
390
  for line in GitPuller(
362
391
  args.git_url,
363
392
  args.repo_dir,
364
- branch=args.branch_name if args.branch_name else None
393
+ branch=args.branch_name if args.branch_name else None,
394
+ backup=args.backup if args.backup else False,
365
395
  ).pull():
366
396
  print(line)
367
397