Add repo namespaces
Currently only available through manual make_app() call
Jonas Haag
4 months ago
22 | 22 | self.ctags_policy = ctags_policy |
23 | 23 | |
24 | 24 | valid_repos, invalid_repos = self.load_repos(repo_paths) |
25 | self.valid_repos = {repo.name: repo for repo in valid_repos} | |
26 | self.invalid_repos = {repo.name: repo for repo in invalid_repos} | |
25 | self.valid_repos = {repo.namespaced_name: repo for repo in valid_repos} | |
26 | self.invalid_repos = {repo.namespaced_name: repo for repo in invalid_repos} | |
27 | 27 | |
28 | 28 | flask.Flask.__init__(self, __name__) |
29 | 29 | |
71 | 71 | ('download', '/<repo>/tarball/<path:rev>/'), |
72 | 72 | ]: |
73 | 73 | self.add_url_rule(rule, view_func=getattr(views, endpoint)) |
74 | if "<repo>" in rule: | |
75 | self.add_url_rule( | |
76 | "/~<namespace>" + rule, view_func=getattr(views, endpoint) | |
77 | ) | |
74 | 78 | # fmt: on |
75 | 79 | |
76 | 80 | def should_use_ctags(self, git_repo, git_commit): |
86 | 90 | def load_repos(self, repo_paths): |
87 | 91 | valid_repos = [] |
88 | 92 | invalid_repos = [] |
89 | for path in repo_paths: | |
90 | try: | |
91 | valid_repos.append(FancyRepo(path)) | |
92 | except NotGitRepository: | |
93 | invalid_repos.append(InvalidRepo(path)) | |
93 | for namespace, paths in repo_paths.items(): | |
94 | for path in paths: | |
95 | try: | |
96 | valid_repos.append(FancyRepo(path, namespace)) | |
97 | except NotGitRepository: | |
98 | invalid_repos.append(InvalidRepo(path, namespace)) | |
94 | 99 | return valid_repos, invalid_repos |
95 | 100 | |
96 | 101 | |
107 | 112 | """ |
108 | 113 | Returns a WSGI app with all the features (smarthttp, authentication) |
109 | 114 | already patched in. |
110 | ||
111 | :param repo_paths: List of paths of repositories to serve. | |
115 | :param repo_paths: Repositories to serve. This can either be a list of paths | |
116 | or dictionary of the following form: | |
117 | { | |
118 | "namespace1": [list of paths of repositories], | |
119 | "namespace2": [list of paths of repositories], | |
120 | ... | |
121 | None: [list of paths of repositories without namespace] | |
122 | } | |
112 | 123 | :param site_name: Name of the Web site (e.g. "John Doe's Git Repositories") |
113 | 124 | :param use_smarthttp: Enable Git Smart HTTP mode, which makes it possible to |
114 | 125 | pull from the served repositories. If `htdigest_file` is set as well, |
140 | 151 | raise ValueError( |
141 | 152 | "'htdigest_file' set without 'use_smarthttp' or 'require_browser_auth'" |
142 | 153 | ) |
154 | if not isinstance(repo_paths, dict): | |
155 | # If repos is given as a flat list, put all repos under the "no namespace" namespace | |
156 | repo_paths = {None: repo_paths} | |
143 | 157 | |
144 | 158 | app = Klaus( |
145 | 159 | repo_paths, |
152 | 166 | if use_smarthttp: |
153 | 167 | # `path -> Repo` mapping for Dulwich's web support |
154 | 168 | dulwich_backend = dulwich.server.DictBackend( |
155 | {"/" + name: repo for name, repo in app.valid_repos.items()} | |
169 | { | |
170 | "/" + namespaced_name: repo | |
171 | for namespaced_name, repo in app.valid_repos.items() | |
172 | } | |
156 | 173 | ) |
157 | 174 | # Dulwich takes care of all Git related requests/URLs |
158 | 175 | # and passes through everything else to klaus |
176 | 193 | # Git will never call /<repo-name>/git-receive-pack if authentication |
177 | 194 | # failed for /info/refs, but since it's used to upload stuff to the server |
178 | 195 | # we must secure it anyway for security reasons. |
179 | PATTERN = r"^/[^/]+/(info/refs\?service=git-receive-pack|git-receive-pack)$" | |
196 | PATTERN = ( | |
197 | r"^/(~[^/]+/)?[^/]+/(info/refs\?service=git-receive-pack|git-receive-pack)$" | |
198 | ) | |
180 | 199 | if unauthenticated_push: |
181 | 200 | # DANGER ZONE: Don't require authentication for push'ing |
182 | 201 | app.wsgi_app = dulwich_wrapped_app |
32 | 32 | class FancyRepo(dulwich.repo.Repo): |
33 | 33 | """A wrapper around Dulwich's Repo that adds some helper methods.""" |
34 | 34 | |
35 | def __init__(self, path, namespace): | |
36 | super().__init__(path) | |
37 | self.namespace = namespace | |
38 | ||
35 | 39 | @property |
36 | 40 | def name(self): |
37 | 41 | return repo_human_name(self.path) |
42 | ||
43 | @property | |
44 | def namespaced_name(self): | |
45 | if self.namespace: | |
46 | return f"~{self.namespace}/{self.name}" | |
47 | else: | |
48 | return self.name | |
38 | 49 | |
39 | 50 | # TODO: factor out stuff into dulwich |
40 | 51 | def get_last_updated_at(self): |
342 | 353 | class InvalidRepo: |
343 | 354 | """Represent an invalid repository and store pertinent data.""" |
344 | 355 | |
345 | def __init__(self, path): | |
356 | def __init__(self, path, namespace): | |
346 | 357 | self.path = path |
358 | self.namespace = namespace | |
347 | 359 | |
348 | 360 | @property |
349 | 361 | def name(self): |
350 | 362 | return repo_human_name(self.path) |
363 | ||
364 | @property | |
365 | def namespaced_name(self): | |
366 | if self.namespace: | |
367 | return f"~{self.namespace}/{self.name}" | |
368 | else: | |
369 | return self.name |
5 | 5 | |
6 | 6 | {% block breadcrumbs %} |
7 | 7 | <span> |
8 | <a href="{{ url_for('index', repo=repo.name) }}">{{ repo.name }}</a> | |
8 | <a href="{{ url_for('index', repo=repo.name, namespace=namespace) }}">{{ repo.namespaced_name }}</a> | |
9 | 9 | <span class=slash>/</span> |
10 | <a href="{{ url_for('history', repo=repo.name, rev=rev) }}">{{ rev|shorten_sha1 }}</a> | |
10 | <a href="{{ url_for('history', repo=repo.name, namespace=namespace, rev=rev) }}">{{ rev|shorten_sha1 }}</a> | |
11 | 11 | </span> |
12 | 12 | |
13 | 13 | {% if subpaths %} |
16 | 16 | {% if loop.last %} |
17 | 17 | <a href="">{{ name|force_unicode }}</a> |
18 | 18 | {% else %} |
19 | <a href="{{ url_for('history', repo=repo.name, rev=rev, path=subpath) }}">{{ name|force_unicode }}</a> | |
19 | <a href="{{ url_for('history', repo=repo.name, namespace=namespace, rev=rev, path=subpath) }}">{{ name|force_unicode }}</a> | |
20 | 20 | <span class=slash>/</span> |
21 | 21 | {% endif %} |
22 | 22 | {% endfor %} |
30 | 30 | <div> |
31 | 31 | <ul class=branches> |
32 | 32 | {% for branch in branches %} |
33 | <li><a href="{{ url_for(view, repo=repo.name, rev=branch, path=path) }}">{{ branch }}</a></li> | |
33 | <li><a href="{{ url_for(view, repo=repo.name, namespace=namespace, rev=branch, path=path) }}">{{ branch }}</a></li> | |
34 | 34 | {% endfor %} |
35 | 35 | </ul> |
36 | 36 | {% if tags %} |
37 | 37 | <ul class=tags> |
38 | 38 | {% for tag in tags %} |
39 | <li><a href="{{ url_for(view, repo=repo.name, rev=tag, path=path) }}">{{ tag }}</a></li> | |
39 | <li><a href="{{ url_for(view, repo=repo.name, namespace=namespace, rev=tag, path=path) }}">{{ tag }}</a></li> | |
40 | 40 | {% endfor %} |
41 | 41 | </ul> |
42 | 42 | {% endif %} |
11 | 11 | <h2> |
12 | 12 | {{ filename|force_unicode }} |
13 | 13 | <span> |
14 | @<a href="{{ url_for('commit', repo=repo.name, rev=rev) }}">{{ rev|shorten_sha1 }}</a> | |
14 | @<a href="{{ url_for('commit', repo=repo.name, namespace=namespace, rev=rev) }}">{{ rev|shorten_sha1 }}</a> | |
15 | 15 | </span> |
16 | 16 | </h2> |
17 | 17 | {% if not can_render %} |
26 | 26 | {%- if commit == None %} |
27 | 27 | |
28 | 28 | {%- else %} |
29 | <a href="{{ url_for('commit', repo=repo.name, rev=commit) }}">{{ commit | shorten_sha1 }}</a> | |
29 | <a href="{{ url_for('commit', repo=repo.name, namespace=namespace, rev=commit) }}">{{ commit | shorten_sha1 }}</a> | |
30 | 30 | {%- endif -%} |
31 | 31 | {%- endfor -%} |
32 | 32 | </pre> |
4 | 4 | {% if n is none %} |
5 | 5 | <span class=n>...</span> |
6 | 6 | {% else %} |
7 | <a href="{{ url_for('history', repo=repo.name, rev=rev, path=path)}}?page={{n}}" class=n>{{ n }}</a> | |
7 | <a href="{{ url_for('history', repo=repo.name, namespace=namespace, rev=rev, path=path)}}?page={{n}}" class=n>{{ n }}</a> | |
8 | 8 | {% endif %} |
9 | 9 | {% endfor %} |
10 | 10 | {% endif %} |
11 | 11 | {% if more_commits %} |
12 | <a href="{{ url_for('history', repo=repo.name, rev=rev, path=path)}}?page={{page+1}}">»»</a> | |
12 | <a href="{{ url_for('history', repo=repo.name, namespace=namespace, rev=rev, path=path)}}?page={{page+1}}">»»</a> | |
13 | 13 | {% elif page %} |
14 | 14 | <span>»»</span> |
15 | 15 | {% endif%} |
31 | 31 | Commit History |
32 | 32 | {% endif %} |
33 | 33 | <span> |
34 | @<a href="{{ url_for('index', repo=repo.name, rev=rev) }}">{{ rev }}</a> | |
34 | @<a href="{{ url_for('index', repo=repo.name, namespace=namespace, rev=rev) }}">{{ rev }}</a> | |
35 | 35 | </span> |
36 | 36 | {% if USE_SMARTHTTP %} |
37 | <code>git clone {{ url_for('index', repo=repo.name, _external=True) }}</code> | |
37 | <code>git clone {{ url_for('index', repo=repo.name, namespace=namespace, _external=True) }}</code> | |
38 | 38 | {% endif %} |
39 | 39 | {% if repo.cloneurl %} |
40 | 40 | <code>git clone {{ repo.cloneurl }}</code> |
46 | 46 | <ul> |
47 | 47 | {% for commit in history %} |
48 | 48 | <li> |
49 | <a class=commit href="{{ url_for('commit', repo=repo.name, rev=commit.id|force_unicode) }}"> | |
49 | <a class=commit href="{{ url_for('commit', repo=repo.name, namespace=namespace, rev=commit.id|force_unicode) }}"> | |
50 | 50 | <span class=line1> |
51 | 51 | <span>{{ commit.message|force_unicode|shorten_message }}</span> |
52 | 52 | </span> |
28 | 28 | <li> |
29 | 29 | <a |
30 | 30 | {% if last_updated_at %} |
31 | href="{{ url_for('index', repo=repo.name) }}" | |
31 | href="{{ url_for('index', namespace=repo.namespace, repo=repo.name) }}" | |
32 | 32 | {% endif %} |
33 | 33 | > |
34 | <div class=name>{{ repo.name }}</div> | |
34 | <div class=name>{{ repo.namespaced_name }}</div> | |
35 | 35 | {% if description %} |
36 | 36 | <div class=description>{{ description }}</div> |
37 | 37 | {% endif %} |
53 | 53 | <ul class="repolist invalid"> |
54 | 54 | {% for repo in invalid_repos %} |
55 | 55 | <li> |
56 | <div class=name>{{ repo.name }}</div> | |
56 | <div class=name>{{ repo.namespaced_name }}</div> | |
57 | 57 | <div class=reason> |
58 | 58 | Invalid git repository |
59 | 59 | </div> |
8 | 8 | <h2> |
9 | 9 | {{ path|force_unicode }} |
10 | 10 | <span> |
11 | @<a href="{{ url_for('commit', repo=repo.name, rev=rev) }}">{{ rev|shorten_sha1 }}</a> | |
11 | @<a href="{{ url_for('commit', repo=repo.name, namespace=namespace, rev=rev) }}">{{ rev|shorten_sha1 }}</a> | |
12 | 12 | </span> |
13 | 13 | </h2> |
14 | 14 | <p>The path at {{ path|force_unicode }} contains a submodule, revision {{ submodule_rev }}. {% if submodule_url %} It can be checked out from <a href="{{ submodule_url }}">{{ submodule_url }}</a>. {% endif %} |
0 | 0 | <div class=tree> |
1 | <h2>Tree @<a href="{{ url_for('commit', repo=repo.name, rev=rev) }}">{{ rev|shorten_sha1 }}</a> | |
2 | <span>(<a href="{{ url_for('download', repo=repo.name, rev=rev) }}">Download .tar.gz</a>)</span> | |
1 | <h2>Tree @<a href="{{ url_for('commit', namespace=namespace, repo=repo.name, rev=rev) }}">{{ rev|shorten_sha1 }}</a> | |
2 | <span>(<a href="{{ url_for('download', namespace=namespace, repo=repo.name, rev=rev) }}">Download .tar.gz</a>)</span> | |
3 | 3 | </h2> |
4 | 4 | <ul> |
5 | 5 | {% for name, fullpath in root_tree.dirs %} |
6 | <li><a href="{{ url_for('history', repo=repo.name, rev=rev, path=fullpath) }}" class=dir>{{ name }}</a></li> | |
6 | <li><a href="{{ url_for('history', namespace=namespace, repo=repo.name, rev=rev, path=fullpath) }}" class=dir>{{ name }}</a></li> | |
7 | 7 | {% endfor %} |
8 | 8 | {% for name, fullpath in root_tree.submodules %} |
9 | <li><a href="{{ url_for('submodule', repo=repo.name, rev=rev, path=fullpath) }}" class=submodule>{{ name }}</a></li> | |
9 | <li><a href="{{ url_for('submodule', namespace=namespace, repo=repo.name, rev=rev, path=fullpath) }}" class=submodule>{{ name }}</a></li> | |
10 | 10 | {% endfor %} |
11 | 11 | {% for name, fullpath in root_tree.files %} |
12 | <li><a href="{{ url_for('blob', repo=repo.name, rev=rev, path=fullpath) }}">{{ name }}</a></li> | |
12 | <li><a href="{{ url_for('blob', namespace=namespace, repo=repo.name, rev=rev, path=fullpath) }}">{{ name }}</a></li> | |
13 | 13 | {% endfor %} |
14 | 14 | </ul> |
15 | 15 | </div> |
7 | 7 | |
8 | 8 | {% include 'tree.inc.html' %} |
9 | 9 | |
10 | {% set raw_url = url_for('raw', repo=repo.name, rev=rev, path=path) %} | |
10 | {% set raw_url = url_for('raw', repo=repo.name, namespace=namespace, rev=rev, path=path) %} | |
11 | 11 | {% macro not_shown(reason) %} |
12 | 12 | <div> |
13 | 13 | ({{ reason }} not shown — <a href="{{ raw_url }}">Download file</a>) |
18 | 18 | <h2> |
19 | 19 | {{ filename|force_unicode }} |
20 | 20 | <span> |
21 | @<a href="{{ url_for('commit', repo=repo.name, rev=rev) }}">{{ rev|shorten_sha1 }}</a> | |
21 | @<a href="{{ url_for('commit', repo=repo.name, namespace=namespace, rev=rev) }}">{{ rev|shorten_sha1 }}</a> | |
22 | 22 | — |
23 | 23 | {% if is_markup %} |
24 | 24 | {% if render_markup %} |
29 | 29 | · |
30 | 30 | {% endif %} |
31 | 31 | <a href="{{ raw_url }}">raw</a> |
32 | · <a href="{{ url_for('history', repo=repo.name, rev=rev, path=path) }}">history</a> | |
32 | · <a href="{{ url_for('history', repo=repo.name, namespace=namespace, rev=rev, path=path) }}">history</a> | |
33 | 33 | {% if not is_binary and not too_large %} |
34 | · <a href="{{ url_for('blame', repo=repo.name, rev=rev, path=path) }}">blame</a> | |
34 | · <a href="{{ url_for('blame', repo=repo.name, namespace=namespace, rev=rev, path=path) }}">blame</a> | |
35 | 35 | {% endif %} |
36 | 36 | </span> |
37 | 37 | </h2> |
32 | 32 | and <span class=deletions>{{ summary.ndeletions }} deletion(s)</span>. |
33 | 33 | </span> |
34 | 34 | <span> |
35 | <a href="{{ url_for('patch', repo=repo.name, rev=commit.id|force_unicode) }}">Raw diff</a> | |
35 | <a href="{{ url_for('patch', repo=repo.name, namespace=namespace, rev=commit.id|force_unicode) }}">Raw diff</a> | |
36 | 36 | </span> |
37 | 37 | <span> |
38 | 38 | <a href=# onclick="toggler.collapseAll('.file'); return false">Collapse all</a> |
62 | 62 | {% if file.new_filename == '/dev/null' %} |
63 | 63 | <del>{{ file.old_filename|force_unicode }}</del> |
64 | 64 | {% else %} |
65 | <a href="{{ url_for('blob', repo=repo.name, rev=rev, path=file.new_filename) }}"> | |
65 | <a href="{{ url_for('blob', repo=repo.name, namespace=namespace, rev=rev, path=file.new_filename) }}"> | |
66 | 66 | {{ file.new_filename|force_unicode }} |
67 | 67 | </a> |
68 | 68 | {% endif %} |
54 | 54 | search_query = request.args.get("q") or "" |
55 | 55 | |
56 | 56 | if search_query: |
57 | repos = [r for r in repos if search_query.lower() in r.name.lower()] | |
57 | repos = [r for r in repos if search_query.lower() in r.namespaced_name.lower()] | |
58 | 58 | invalid_repos = [ |
59 | r for r in invalid_repos if search_query.lower() in r.name.lower() | |
59 | r | |
60 | for r in invalid_repos | |
61 | if search_query.lower() in r.namespaced_name.lower() | |
60 | 62 | ] |
61 | 63 | |
62 | 64 | if order_by == "name": |
63 | sort_key = lambda repo: repo.name | |
65 | sort_key = lambda repo: repo.namespaced_name | |
64 | 66 | else: |
65 | 67 | sort_key = lambda repo: ( |
66 | 68 | -(repo.fast_get_last_updated_at() or -1), |
67 | repo.name, | |
69 | repo.namespaced_name, | |
68 | 70 | ) |
69 | 71 | |
70 | 72 | repos = sorted(repos, key=sort_key) |
71 | invalid_repos = sorted(invalid_repos, key=lambda repo: repo.name) | |
73 | invalid_repos = sorted(invalid_repos, key=lambda repo: repo.namespaced_name) | |
72 | 74 | |
73 | 75 | return render_template( |
74 | 76 | "repo_list.html", |
85 | 87 | return current_app.send_static_file("robots.txt") |
86 | 88 | |
87 | 89 | |
88 | def _get_repo_and_rev(repo, rev=None, path=None): | |
90 | def _get_repo_and_rev(repo, namespace=None, rev=None, path=None): | |
89 | 91 | if path and rev: |
90 | 92 | rev += "/" + path.rstrip("/") |
91 | 93 | |
94 | if namespace: | |
95 | repo_key = f"~{namespace}/{repo}" | |
96 | else: | |
97 | repo_key = repo | |
92 | 98 | try: |
93 | repo = current_app.valid_repos[repo] | |
99 | repo = current_app.valid_repos[repo_key] | |
94 | 100 | except KeyError: |
95 | 101 | raise NotFound("No such repository %r" % repo) |
96 | 102 | |
144 | 150 | self.view_name = view_name |
145 | 151 | self.context = {} |
146 | 152 | |
147 | def dispatch_request(self, repo, rev=None, path=""): | |
153 | def dispatch_request(self, repo, namespace=None, rev=None, path=""): | |
148 | 154 | """Dispatch repository, revision (if any) and path (if any). To retain |
149 | 155 | compatibility with :func:`url_for`, view routing uses two arguments: |
150 | 156 | rev and path, although a single path is sufficient (from Git's point of |
157 | 163 | |
158 | 164 | [1] https://github.com/jonashaag/klaus/issues/36#issuecomment-23990266 |
159 | 165 | """ |
160 | self.make_template_context(repo, rev, path.strip("/")) | |
166 | self.make_template_context(repo, namespace, rev, path.strip("/")) | |
161 | 167 | return self.get_response() |
162 | 168 | |
163 | 169 | def get_response(self): |
164 | 170 | return render_template(self.template_name, **self.context) |
165 | 171 | |
166 | def make_template_context(self, repo, rev, path): | |
167 | repo, rev, path, commit = _get_repo_and_rev(repo, rev, path) | |
172 | def make_template_context(self, repo, namespace, rev, path): | |
173 | repo, rev, path, commit = _get_repo_and_rev(repo, namespace, rev, path) | |
168 | 174 | |
169 | 175 | try: |
170 | 176 | blob_or_tree = repo.get_blob_or_tree(commit, path) |
174 | 180 | self.context = { |
175 | 181 | "view": self.view_name, |
176 | 182 | "repo": repo, |
183 | "namespace": namespace, | |
177 | 184 | "rev": rev, |
178 | 185 | "commit": commit, |
179 | 186 | "branches": repo.get_branch_names(exclude=rev), |
284 | 291 | super(IndexView, self).make_template_context(*args) |
285 | 292 | |
286 | 293 | self.context["base_href"] = url_for( |
287 | "blob", repo=self.context["repo"].name, rev=self.context["rev"], path="" | |
294 | "blob", | |
295 | repo=self.context["repo"].namespaced_name, | |
296 | rev=self.context["rev"], | |
297 | path="", | |
288 | 298 | ) |
289 | 299 | |
290 | 300 | self.context["page"] = 0 |
342 | 352 | |
343 | 353 | template_name = "submodule.html" |
344 | 354 | |
345 | def make_template_context(self, repo, rev, path): | |
346 | repo, rev, path, commit = _get_repo_and_rev(repo, rev, path) | |
355 | def make_template_context(self, repo, namespace, rev, path): | |
356 | repo, rev, path, commit = _get_repo_and_rev(repo, namespace, rev, path) | |
347 | 357 | |
348 | 358 | try: |
349 | 359 | submodule_rev = tree_lookup_path( |
392 | 402 | raise ImportError("Ctags enabled but python-ctags not installed") |
393 | 403 | ctags_base_url = url_for( |
394 | 404 | self.view_name, |
395 | repo=self.context["repo"].name, | |
405 | repo=self.context["repo"].namespaced_name, | |
396 | 406 | rev=self.context["rev"], |
397 | 407 | path="", |
398 | 408 | ) |
412 | 422 | force_unicode(self.context["blob_or_tree"].data), |
413 | 423 | self.context["filename"], |
414 | 424 | render_markup, |
415 | **ctags_args | |
425 | **ctags_args, | |
416 | 426 | ) |
417 | 427 | |
418 | 428 | def make_template_context(self, *args): |
11 | 11 | with serve(): |
12 | 12 | for file in ["binary", "image.jpg", "toolarge"]: |
13 | 13 | response = requests.get( |
14 | TEST_REPO_DONT_RENDER_URL + "blob/HEAD/" + file | |
14 | UNAUTH_TEST_REPO_DONT_RENDER_URL + "blob/HEAD/" + file | |
15 | 15 | ).text |
16 | 16 | assert "blame" not in response |
17 | 17 | |
21 | 21 | with serve(): |
22 | 22 | for file in ["binary", "image.jpg", "toolarge"]: |
23 | 23 | response = requests.get( |
24 | TEST_REPO_DONT_RENDER_URL + "blame/HEAD/" + file | |
24 | UNAUTH_TEST_REPO_DONT_RENDER_URL + "blame/HEAD/" + file | |
25 | 25 | ).text |
26 | 26 | assert "Can't show blame" in response |
4 | 4 | except ImportError: |
5 | 5 | pass |
6 | 6 | |
7 | import subprocess | |
8 | ||
7 | 9 | import pytest |
10 | import requests | |
11 | ||
8 | 12 | |
9 | 13 | from klaus.contrib import app_args |
10 | 14 | from .utils import * |
11 | from .test_make_app import can_reach_unauth, can_push_auth | |
12 | 15 | |
13 | 16 | |
14 | 17 | def check_env(env, expected_args, expected_kwargs): |
40 | 43 | monkeypatch.setattr(os, "environ", os.environ.copy()) |
41 | 44 | check_env( |
42 | 45 | { |
43 | "KLAUS_REPOS": TEST_REPO, | |
46 | "KLAUS_REPOS": TEST_REPO_NO_NAMESPACE, | |
44 | 47 | "KLAUS_SITE_NAME": TEST_SITE_NAME, |
45 | 48 | "KLAUS_HTDIGEST_FILE": HTDIGEST_FILE, |
46 | 49 | "KLAUS_USE_SMARTHTTP": "yes", |
49 | 52 | "KLAUS_UNAUTHENTICATED_PUSH": "0", |
50 | 53 | "KLAUS_CTAGS_POLICY": "ALL", |
51 | 54 | }, |
52 | ([TEST_REPO], TEST_SITE_NAME), | |
55 | ([TEST_REPO_NO_NAMESPACE], TEST_SITE_NAME), | |
53 | 56 | dict( |
54 | 57 | htdigest_file=HTDIGEST_FILE, |
55 | 58 | use_smarthttp=True, |
67 | 70 | with pytest.raises(ValueError): |
68 | 71 | check_env( |
69 | 72 | { |
70 | "KLAUS_REPOS": TEST_REPO, | |
73 | "KLAUS_REPOS": TEST_REPO_NO_NAMESPACE, | |
71 | 74 | "KLAUS_SITE_NAME": TEST_SITE_NAME, |
72 | 75 | "KLAUS_HTDIGEST_FILE": HTDIGEST_FILE, |
73 | 76 | "KLAUS_USE_SMARTHTTP": "unsupported", |
80 | 83 | def test_wsgi(monkeypatch): |
81 | 84 | """Test start of wsgi app""" |
82 | 85 | monkeypatch.setattr(os, "environ", os.environ.copy()) |
83 | os.environ["KLAUS_REPOS"] = TEST_REPO | |
86 | os.environ["KLAUS_REPOS"] = TEST_REPO_NO_NAMESPACE | |
84 | 87 | os.environ["KLAUS_SITE_NAME"] = TEST_SITE_NAME |
85 | 88 | from klaus.contrib import wsgi |
86 | 89 | |
99 | 102 | def test_wsgi_autoreload(monkeypatch): |
100 | 103 | """Test start of wsgi autoreload app""" |
101 | 104 | monkeypatch.setattr(os, "environ", os.environ.copy()) |
102 | os.environ["KLAUS_REPOS_ROOT"] = TEST_REPO_ROOT | |
105 | os.environ["KLAUS_REPOS_ROOT"] = TEST_REPO_NO_NAMESPACE_ROOT | |
103 | 106 | os.environ["KLAUS_SITE_NAME"] = TEST_SITE_NAME |
104 | 107 | from klaus.contrib import wsgi_autoreload, wsgi_autoreloading |
105 | 108 | |
113 | 116 | reload(wsgi_autoreloading) |
114 | 117 | with serve_app(wsgi_autoreload.application): |
115 | 118 | assert can_push_auth() |
119 | ||
120 | ||
121 | def can_reach_unauth(): | |
122 | return _check_http200(_GET_unauth, TEST_REPO_NO_NAMESPACE_BASE_URL) | |
123 | ||
124 | ||
125 | def can_push_auth(): | |
126 | return _can_push(_GET_auth, AUTH_TEST_REPO_NO_NAMESPACE_URL) | |
127 | ||
128 | ||
129 | def _can_push(http_get, url): | |
130 | return any( | |
131 | [ | |
132 | _check_http200( | |
133 | http_get, | |
134 | TEST_REPO_NO_NAMESPACE_BASE_URL + "info/refs?service=git-receive-pack", | |
135 | ), | |
136 | _check_http200( | |
137 | http_get, TEST_REPO_NO_NAMESPACE_BASE_URL + "git-receive-pack" | |
138 | ), | |
139 | subprocess.call(["git", "push", url, "master"], cwd=TEST_REPO_NO_NAMESPACE) | |
140 | == 0, | |
141 | ] | |
142 | ) | |
143 | ||
144 | ||
145 | def _GET_unauth(url=""): | |
146 | return requests.get( | |
147 | UNAUTH_TEST_SERVER + url, | |
148 | auth=requests.auth.HTTPDigestAuth("invalid", "password"), | |
149 | ) | |
150 | ||
151 | ||
152 | def _GET_auth(url=""): | |
153 | return requests.get( | |
154 | AUTH_TEST_SERVER + url, | |
155 | auth=requests.auth.HTTPDigestAuth("testuser", "testpassword"), | |
156 | ) | |
157 | ||
158 | ||
159 | def _check_http200(http_get, url): | |
160 | return http_get(url).status_code == 200 |
10 | 10 | import requests.auth |
11 | 11 | |
12 | 12 | from .utils import * |
13 | ||
14 | ||
15 | def test_make_app_using_list(): | |
16 | app = klaus.make_app(REPOS, TEST_SITE_NAME) | |
17 | with serve_app(app): | |
18 | response = requests.get(UNAUTH_TEST_SERVER).text | |
19 | assert TEST_REPO_NO_NEWLINE_BASE_URL in response | |
13 | 20 | |
14 | 21 | |
15 | 22 | def test_htdigest_file_without_smarthttp_or_require_browser_auth(): |
49 | 56 | else: |
50 | 57 | checks = ["can_%s_unauth" % check, "can_%s_auth" % check] |
51 | 58 | for check in checks: |
52 | assert globals()[check]() == permitted | |
59 | assert globals()[check]() == permitted, check | |
53 | 60 | |
54 | 61 | return test |
55 | 62 | |
115 | 122 | |
116 | 123 | # Reach |
117 | 124 | def can_reach_unauth(): |
118 | return _check_http200(_GET_unauth, "test_repo") | |
125 | return _check_http200(_GET_unauth, TEST_REPO_BASE_URL) | |
119 | 126 | |
120 | 127 | |
121 | 128 | def can_reach_auth(): |
122 | return _check_http200(_GET_auth, "test_repo") | |
129 | return _check_http200(_GET_auth, TEST_REPO_BASE_URL) | |
123 | 130 | |
124 | 131 | |
125 | 132 | # Clone |
136 | 143 | try: |
137 | 144 | return any( |
138 | 145 | [ |
139 | "git clone" in http_get(TEST_REPO_URL).text, | |
146 | "git clone" in http_get(TEST_REPO_BASE_URL).text, | |
140 | 147 | _check_http200( |
141 | http_get, TEST_REPO_URL + "info/refs?service=git-upload-pack" | |
148 | http_get, TEST_REPO_BASE_URL + "info/refs?service=git-upload-pack" | |
142 | 149 | ), |
143 | 150 | subprocess.call(["git", "clone", url, tmp]) == 0, |
144 | 151 | ] |
160 | 167 | return any( |
161 | 168 | [ |
162 | 169 | _check_http200( |
163 | http_get, TEST_REPO_URL + "info/refs?service=git-receive-pack" | |
170 | http_get, TEST_REPO_BASE_URL + "info/refs?service=git-receive-pack" | |
164 | 171 | ), |
165 | _check_http200(http_get, TEST_REPO_URL + "git-receive-pack"), | |
172 | _check_http200(http_get, TEST_REPO_BASE_URL + "git-receive-pack"), | |
166 | 173 | subprocess.call(["git", "push", url, "master"], cwd=TEST_REPO) == 0, |
167 | 174 | ] |
168 | 175 | ) |
190 | 197 | def _ctags_enabled(ref, filename): |
191 | 198 | response = requests.get(UNAUTH_TEST_REPO_URL + "blob/%s/%s" % (ref, filename)) |
192 | 199 | assert response.status_code == 200, response.text |
193 | href = '<a href="/%sblob/%s/%s#L-1">' % (TEST_REPO_URL, ref, filename) | |
200 | href = '<a href="/%sblob/%s/%s#L-1">' % (TEST_REPO_BASE_URL, ref, filename) | |
194 | 201 | return href in response.text |
195 | 202 | |
196 | 203 | |
209 | 216 | |
210 | 217 | |
211 | 218 | def _check_http200(http_get, url): |
212 | try: | |
213 | return http_get(url).status_code == 200 | |
214 | except: | |
215 | return False | |
219 | return http_get(url).status_code == 200 |
24 | 24 | assert TEST_INVALID_REPO_NAME in response |
25 | 25 | |
26 | 26 | |
27 | def test_repo_list_search_namespace(): | |
28 | with serve(): | |
29 | response = requests.get(UNAUTH_TEST_SERVER + "?q=" + NAMESPACE).text | |
30 | assert TEST_REPO_BASE_URL in response | |
31 | assert not TEST_REPO_DONT_RENDER_BASE_URL in response | |
32 | assert not TEST_REPO_NO_NEWLINE_BASE_URL in response | |
33 | assert not TEST_INVALID_REPO_NAME in response | |
34 | ||
35 | ||
27 | 36 | def test_download(): |
28 | 37 | with serve(): |
29 | 38 | response = requests.get(UNAUTH_TEST_REPO_URL + "tarball/master", stream=True) |
35 | 44 | |
36 | 45 | def test_no_newline_at_end_of_file(): |
37 | 46 | with serve(): |
38 | response = requests.get(TEST_REPO_NO_NEWLINE_URL + "commit/HEAD/").text | |
47 | response = requests.get(UNAUTH_TEST_REPO_NO_NEWLINE_URL + "commit/HEAD/").text | |
39 | 48 | assert response.count("No newline at end of file") == 1 |
40 | 49 | |
41 | 50 | |
42 | 51 | def test_dont_render_binary(): |
43 | 52 | with serve(): |
44 | response = requests.get(TEST_REPO_DONT_RENDER_URL + "blob/HEAD/binary").text | |
53 | response = requests.get( | |
54 | UNAUTH_TEST_REPO_DONT_RENDER_URL + "blob/HEAD/binary" | |
55 | ).text | |
45 | 56 | assert "Binary data not shown" in response |
46 | 57 | |
47 | 58 | |
48 | 59 | def test_render_image(): |
49 | 60 | with serve(): |
50 | response = requests.get(TEST_REPO_DONT_RENDER_URL + "blob/HEAD/image.jpg").text | |
61 | response = requests.get( | |
62 | UNAUTH_TEST_REPO_DONT_RENDER_URL + "blob/HEAD/image.jpg" | |
63 | ).text | |
51 | 64 | assert '<img src="/dont-render/raw/HEAD/image.jpg"' in response |
52 | 65 | |
53 | 66 | |
54 | 67 | def test_dont_render_large_file(): |
55 | 68 | with serve(): |
56 | response = requests.get(TEST_REPO_DONT_RENDER_URL + "blob/HEAD/toolarge").text | |
69 | response = requests.get( | |
70 | UNAUTH_TEST_REPO_DONT_RENDER_URL + "blob/HEAD/toolarge" | |
71 | ).text | |
57 | 72 | assert "Large file not shown" in response |
58 | 73 | |
59 | 74 |
8 | 8 | TEST_SITE_NAME = "Some site" |
9 | 9 | HTDIGEST_FILE = "tests/credentials.htdigest" |
10 | 10 | |
11 | UNAUTH_TEST_SERVER = "http://invalid:password@localhost:9876/" | |
12 | AUTH_TEST_SERVER = "http://testuser:testpassword@localhost:9876/" | |
13 | ||
14 | NAMESPACE = "namespace1" | |
15 | ||
11 | 16 | TEST_REPO = os.path.abspath("tests/repos/build/test_repo") |
12 | 17 | TEST_REPO_ROOT = os.path.abspath("tests/repos/build") |
13 | TEST_REPO_URL = "test_repo/" | |
14 | UNAUTH_TEST_SERVER = "http://invalid:password@localhost:9876/" | |
15 | UNAUTH_TEST_REPO_URL = UNAUTH_TEST_SERVER + TEST_REPO_URL | |
16 | AUTH_TEST_SERVER = "http://testuser:testpassword@localhost:9876/" | |
17 | AUTH_TEST_REPO_URL = AUTH_TEST_SERVER + TEST_REPO_URL | |
18 | TEST_REPO_BASE_URL = f"~{NAMESPACE}/test_repo/" | |
19 | UNAUTH_TEST_REPO_URL = UNAUTH_TEST_SERVER + TEST_REPO_BASE_URL | |
20 | AUTH_TEST_REPO_URL = AUTH_TEST_SERVER + TEST_REPO_BASE_URL | |
21 | ||
22 | TEST_REPO_NO_NAMESPACE = TEST_REPO | |
23 | TEST_REPO_NO_NAMESPACE_ROOT = TEST_REPO_ROOT | |
24 | TEST_REPO_NO_NAMESPACE_BASE_URL = "test_repo/" | |
25 | AUTH_TEST_REPO_NO_NAMESPACE_URL = AUTH_TEST_SERVER + TEST_REPO_NO_NAMESPACE_BASE_URL | |
18 | 26 | |
19 | 27 | TEST_REPO_NO_NEWLINE = os.path.abspath("tests/repos/build/no-newline-at-end-of-file") |
20 | TEST_REPO_NO_NEWLINE_URL = UNAUTH_TEST_SERVER + "no-newline-at-end-of-file/" | |
28 | TEST_REPO_NO_NEWLINE_BASE_URL = "no-newline-at-end-of-file/" | |
29 | UNAUTH_TEST_REPO_NO_NEWLINE_URL = UNAUTH_TEST_SERVER + TEST_REPO_NO_NEWLINE_BASE_URL | |
21 | 30 | |
22 | 31 | TEST_REPO_DONT_RENDER = os.path.abspath("tests/repos/build/dont-render") |
23 | TEST_REPO_DONT_RENDER_URL = UNAUTH_TEST_SERVER + "dont-render/" | |
32 | TEST_REPO_DONT_RENDER_BASE_URL = "dont-render/" | |
33 | UNAUTH_TEST_REPO_DONT_RENDER_URL = UNAUTH_TEST_SERVER + TEST_REPO_DONT_RENDER_BASE_URL | |
24 | 34 | |
25 | 35 | TEST_INVALID_REPO = os.path.abspath("tests/repos/build/invalid_repo") |
36 | TEST_INVALID_REPO_NAME = "invalid_repo" | |
26 | 37 | |
27 | ALL_TEST_REPOS = [ | |
28 | TEST_REPO, | |
29 | TEST_REPO_NO_NEWLINE, | |
30 | TEST_REPO_DONT_RENDER, | |
31 | TEST_INVALID_REPO, | |
32 | ] | |
38 | REPOS = [TEST_REPO_NO_NEWLINE, TEST_REPO_DONT_RENDER, TEST_INVALID_REPO] | |
39 | ALL_TEST_REPOS = {NAMESPACE: [TEST_REPO], None: REPOS} | |
33 | 40 | |
34 | 41 | |
35 | 42 | @contextlib.contextmanager |