pygitweb 0.2.0__tar.gz → 0.2.1__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 (108) hide show
  1. {pygitweb-0.2.0 → pygitweb-0.2.1}/PKG-INFO +11 -11
  2. {pygitweb-0.2.0 → pygitweb-0.2.1}/README.md +10 -10
  3. {pygitweb-0.2.0 → pygitweb-0.2.1}/auth_test.py +172 -1
  4. {pygitweb-0.2.0 → pygitweb-0.2.1}/main.py +2 -2
  5. {pygitweb-0.2.0 → pygitweb-0.2.1}/pygitweb.egg-info/PKG-INFO +11 -11
  6. {pygitweb-0.2.0 → pygitweb-0.2.1}/pygitweb.egg-info/SOURCES.txt +1 -0
  7. {pygitweb-0.2.0 → pygitweb-0.2.1}/pyproject.toml +2 -2
  8. {pygitweb-0.2.0 → pygitweb-0.2.1}/LICENSE +0 -0
  9. {pygitweb-0.2.0 → pygitweb-0.2.1}/__init__.py +0 -0
  10. {pygitweb-0.2.0 → pygitweb-0.2.1}/__meta__.py +0 -0
  11. {pygitweb-0.2.0 → pygitweb-0.2.1}/actions.py +0 -0
  12. {pygitweb-0.2.0 → pygitweb-0.2.1}/addproject_permissions_test.py +0 -0
  13. {pygitweb-0.2.0 → pygitweb-0.2.1}/api/__init__.py +0 -0
  14. {pygitweb-0.2.0 → pygitweb-0.2.1}/api/action.py +0 -0
  15. {pygitweb-0.2.0 → pygitweb-0.2.1}/api/plugins/__init__.py +0 -0
  16. {pygitweb-0.2.0 → pygitweb-0.2.1}/api/plugins/project_zip.py +0 -0
  17. {pygitweb-0.2.0 → pygitweb-0.2.1}/api/project.py +0 -0
  18. {pygitweb-0.2.0 → pygitweb-0.2.1}/api/subpage.py +0 -0
  19. {pygitweb-0.2.0 → pygitweb-0.2.1}/auth.py +0 -0
  20. {pygitweb-0.2.0 → pygitweb-0.2.1}/auth.schema.json +0 -0
  21. {pygitweb-0.2.0 → pygitweb-0.2.1}/auth_config.py +0 -0
  22. {pygitweb-0.2.0 → pygitweb-0.2.1}/auth_oauth.py +0 -0
  23. {pygitweb-0.2.0 → pygitweb-0.2.1}/board_refs.py +0 -0
  24. {pygitweb-0.2.0 → pygitweb-0.2.1}/board_refs_test.py +0 -0
  25. {pygitweb-0.2.0 → pygitweb-0.2.1}/boards_permissions_test.py +0 -0
  26. {pygitweb-0.2.0 → pygitweb-0.2.1}/change_queue.py +0 -0
  27. {pygitweb-0.2.0 → pygitweb-0.2.1}/change_queue_routes_test.py +0 -0
  28. {pygitweb-0.2.0 → pygitweb-0.2.1}/change_queue_test.py +0 -0
  29. {pygitweb-0.2.0 → pygitweb-0.2.1}/comments_permissions_test.py +0 -0
  30. {pygitweb-0.2.0 → pygitweb-0.2.1}/config.py +0 -0
  31. {pygitweb-0.2.0 → pygitweb-0.2.1}/conftest.py +0 -0
  32. {pygitweb-0.2.0 → pygitweb-0.2.1}/dependencies.py +0 -0
  33. {pygitweb-0.2.0 → pygitweb-0.2.1}/formatting.py +0 -0
  34. {pygitweb-0.2.0 → pygitweb-0.2.1}/formatting_test.py +0 -0
  35. {pygitweb-0.2.0 → pygitweb-0.2.1}/git_helpers.py +0 -0
  36. {pygitweb-0.2.0 → pygitweb-0.2.1}/git_helpers_test.py +0 -0
  37. {pygitweb-0.2.0 → pygitweb-0.2.1}/gravatar.py +0 -0
  38. {pygitweb-0.2.0 → pygitweb-0.2.1}/gravatar_test.py +0 -0
  39. {pygitweb-0.2.0 → pygitweb-0.2.1}/hooks_install.py +0 -0
  40. {pygitweb-0.2.0 → pygitweb-0.2.1}/hooks_install_test.py +0 -0
  41. {pygitweb-0.2.0 → pygitweb-0.2.1}/hooks_permissions_test.py +0 -0
  42. {pygitweb-0.2.0 → pygitweb-0.2.1}/merge_requests.py +0 -0
  43. {pygitweb-0.2.0 → pygitweb-0.2.1}/merge_requests_test.py +0 -0
  44. {pygitweb-0.2.0 → pygitweb-0.2.1}/mr_permissions_test.py +0 -0
  45. {pygitweb-0.2.0 → pygitweb-0.2.1}/password_hash.py +0 -0
  46. {pygitweb-0.2.0 → pygitweb-0.2.1}/password_hash_test.py +0 -0
  47. {pygitweb-0.2.0 → pygitweb-0.2.1}/permissions.py +0 -0
  48. {pygitweb-0.2.0 → pygitweb-0.2.1}/permissions_test.py +0 -0
  49. {pygitweb-0.2.0 → pygitweb-0.2.1}/plugin_loader.py +0 -0
  50. {pygitweb-0.2.0 → pygitweb-0.2.1}/project_list_routes_test.py +0 -0
  51. {pygitweb-0.2.0 → pygitweb-0.2.1}/projects.py +0 -0
  52. {pygitweb-0.2.0 → pygitweb-0.2.1}/projects_test.py +0 -0
  53. {pygitweb-0.2.0 → pygitweb-0.2.1}/pygitweb.egg-info/dependency_links.txt +0 -0
  54. {pygitweb-0.2.0 → pygitweb-0.2.1}/pygitweb.egg-info/requires.txt +0 -0
  55. {pygitweb-0.2.0 → pygitweb-0.2.1}/pygitweb.egg-info/top_level.txt +0 -0
  56. {pygitweb-0.2.0 → pygitweb-0.2.1}/sessions.py +0 -0
  57. {pygitweb-0.2.0 → pygitweb-0.2.1}/settings.py +0 -0
  58. {pygitweb-0.2.0 → pygitweb-0.2.1}/settings.schema.json +0 -0
  59. {pygitweb-0.2.0 → pygitweb-0.2.1}/settings_config.py +0 -0
  60. {pygitweb-0.2.0 → pygitweb-0.2.1}/settings_config_test.py +0 -0
  61. {pygitweb-0.2.0 → pygitweb-0.2.1}/settings_permissions_test.py +0 -0
  62. {pygitweb-0.2.0 → pygitweb-0.2.1}/setup.cfg +0 -0
  63. {pygitweb-0.2.0 → pygitweb-0.2.1}/shutdown.py +0 -0
  64. {pygitweb-0.2.0 → pygitweb-0.2.1}/smart_http.py +0 -0
  65. {pygitweb-0.2.0 → pygitweb-0.2.1}/smart_http_test.py +0 -0
  66. {pygitweb-0.2.0 → pygitweb-0.2.1}/static/blob-view.js +0 -0
  67. {pygitweb-0.2.0 → pygitweb-0.2.1}/static/board-refs.js +0 -0
  68. {pygitweb-0.2.0 → pygitweb-0.2.1}/static/board.js +0 -0
  69. {pygitweb-0.2.0 → pygitweb-0.2.1}/static/hook-install.js +0 -0
  70. {pygitweb-0.2.0 → pygitweb-0.2.1}/static/icons/cloud-down.svg +0 -0
  71. {pygitweb-0.2.0 → pygitweb-0.2.1}/static/icons/cloud-exclamation.svg +0 -0
  72. {pygitweb-0.2.0 → pygitweb-0.2.1}/static/icons/cloud-filled.svg +0 -0
  73. {pygitweb-0.2.0 → pygitweb-0.2.1}/static/icons/cloud-storm.svg +0 -0
  74. {pygitweb-0.2.0 → pygitweb-0.2.1}/static/icons/cloud-up.svg +0 -0
  75. {pygitweb-0.2.0 → pygitweb-0.2.1}/static/icons/cloud.svg +0 -0
  76. {pygitweb-0.2.0 → pygitweb-0.2.1}/static/logo.svg +0 -0
  77. {pygitweb-0.2.0 → pygitweb-0.2.1}/static/main.css +0 -0
  78. {pygitweb-0.2.0 → pygitweb-0.2.1}/static/navbar-auth.js +0 -0
  79. {pygitweb-0.2.0 → pygitweb-0.2.1}/static/readme-render.js +0 -0
  80. {pygitweb-0.2.0 → pygitweb-0.2.1}/static/search-page.js +0 -0
  81. {pygitweb-0.2.0 → pygitweb-0.2.1}/static/summary-ref-switcher.js +0 -0
  82. {pygitweb-0.2.0 → pygitweb-0.2.1}/static/themes/solarized.css +0 -0
  83. {pygitweb-0.2.0 → pygitweb-0.2.1}/tasks.py +0 -0
  84. {pygitweb-0.2.0 → pygitweb-0.2.1}/tasks_permissions_test.py +0 -0
  85. {pygitweb-0.2.0 → pygitweb-0.2.1}/tasks_routes_test.py +0 -0
  86. {pygitweb-0.2.0 → pygitweb-0.2.1}/templates/addproject.html +0 -0
  87. {pygitweb-0.2.0 → pygitweb-0.2.1}/templates/blobdiff.html +0 -0
  88. {pygitweb-0.2.0 → pygitweb-0.2.1}/templates/board.html +0 -0
  89. {pygitweb-0.2.0 → pygitweb-0.2.1}/templates/board_card.html +0 -0
  90. {pygitweb-0.2.0 → pygitweb-0.2.1}/templates/commitdiff.html +0 -0
  91. {pygitweb-0.2.0 → pygitweb-0.2.1}/templates/hooks_summary_cell.html +0 -0
  92. {pygitweb-0.2.0 → pygitweb-0.2.1}/templates/login.html +0 -0
  93. {pygitweb-0.2.0 → pygitweb-0.2.1}/templates/merge_request_summary_cell.html +0 -0
  94. {pygitweb-0.2.0 → pygitweb-0.2.1}/templates/merge_request_tag.html +0 -0
  95. {pygitweb-0.2.0 → pygitweb-0.2.1}/templates/pagination.html +0 -0
  96. {pygitweb-0.2.0 → pygitweb-0.2.1}/templates/preamble.html +0 -0
  97. {pygitweb-0.2.0 → pygitweb-0.2.1}/templates/project_settings.html +0 -0
  98. {pygitweb-0.2.0 → pygitweb-0.2.1}/templates/pygit2_settings.html +0 -0
  99. {pygitweb-0.2.0 → pygitweb-0.2.1}/templates/pygitweb_settings.html +0 -0
  100. {pygitweb-0.2.0 → pygitweb-0.2.1}/templates/search.html +0 -0
  101. {pygitweb-0.2.0 → pygitweb-0.2.1}/templates/settings_form_field.html +0 -0
  102. {pygitweb-0.2.0 → pygitweb-0.2.1}/templates/shutdown_ack.html +0 -0
  103. {pygitweb-0.2.0 → pygitweb-0.2.1}/templates/table.html +0 -0
  104. {pygitweb-0.2.0 → pygitweb-0.2.1}/templates/user_profile.html +0 -0
  105. {pygitweb-0.2.0 → pygitweb-0.2.1}/templates_env.py +0 -0
  106. {pygitweb-0.2.0 → pygitweb-0.2.1}/timeline_cache.py +0 -0
  107. {pygitweb-0.2.0 → pygitweb-0.2.1}/validation.py +0 -0
  108. {pygitweb-0.2.0 → pygitweb-0.2.1}/validation_test.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pygitweb
3
- Version: 0.2.0
3
+ Version: 0.2.1
4
4
  Summary: Gitweb reimplementation with FastAPI and Pygit2
5
5
  License-Expression: MIT
6
6
  Requires-Python: >=3.11
@@ -40,7 +40,7 @@ environment variables, and optionally a `.env` file in the working directory via
40
40
 
41
41
  ## Routes
42
42
 
43
- Load the `/docs` page for a detailed view of routes (below the readme) if in debug mode.
43
+ Load the `/docs` page for a detailed view of routes (below the readme).
44
44
 
45
45
  **No-project routes**
46
46
 
@@ -68,8 +68,8 @@ Load the `/docs` page for a detailed view of routes (below the readme) if in deb
68
68
  | heads | `updates`, `pf` | `GET /project/{project}?a=heads` | List branch heads. Supports long-poll subscribe. |
69
69
  | tags | `updates`, `pf` | `GET /project/{project}?a=tags` | List all tags. Supports long-poll subscribe. |
70
70
  | tag | `h` | `GET /project/{project}?a=tag&h=...` | Single tag view (tag ref or hash). |
71
- | commit | — | `GET /project/{project}?a=commit` | Commit information. |
72
- | commitdiff | — | `GET /project/{project}?a=commitdiff` | Commit diff (unified diff rendered with [diff2html](https://github.com/rtfpessoa/diff2html)). |
71
+ | commit | `h` | `GET /project/{project}?a=commit&h=...` | Commit information. |
72
+ | commitdiff | `h` | `GET /project/{project}?a=commitdiff&h=...` | Commit diff (unified diff rendered with [diff2html](https://github.com/rtfpessoa/diff2html)). |
73
73
  | patch | `h` | `GET /project/{project}?a=patch&h=...` | Single-commit patch (plain text). |
74
74
  | patches | `h`, `hb` | `GET /project/{project}?a=patches&h=...&hb=...` | Multi-commit patches for range `hb..h` (plain text). |
75
75
  | blobdiff | `h`, `hb`, `f`, `fp` | `GET /project/{project}?a=blobdiff&h=...&hb=...&f=...&fp=...` | Blob diff (two versions of a file) rendered with diff2html. |
@@ -120,16 +120,16 @@ subscribe to the project's change queue. The request is held for up to 30 second
120
120
  just the URL project (the action is still rendered for the URL project).
121
121
 
122
122
  The notify endpoint is intended for loopback use by `pygittools` post-receive hooks (see
123
- `pygittools/hook_samples/post-receive.notify`); production deployments should restrict it
124
- at the reverse proxy.
123
+ `pygittools/hook_samples/post-receive.notify`). The server enforces loopback-only access and
124
+ rejects unknown projects; production deployments should still restrict it at the reverse proxy.
125
125
 
126
126
  ## Hook management
127
127
 
128
- The project summary page exposes an **Update Hook** row that installs / removes the
129
- `update` bundle: `post-receive.notify` (feeds the change queue when refs are pushed) and
130
- `post-commit.notify` (feeds the change queue when a working clone of this repo records
131
- a local commit). Either is sufficient to wake long-poll subscribers; installing both
132
- covers server-side and client-side commit paths.
128
+ The project summary page exposes a **Hooks** row that lists installed hooks and lets you
129
+ install or remove pygittools samples, including the `update` bundle: `post-receive.notify`
130
+ (feeds the change queue when refs are pushed) and `post-commit.notify` (feeds the change
131
+ queue when a working clone of this repo records a local commit). Either is sufficient to
132
+ wake long-poll subscribers; installing both covers server-side and client-side commit paths.
133
133
 
134
134
  The same operations are available programmatically via
135
135
  `POST /project/{name}/hook?name=<sample-or-bundle>&op=<...>`. Bundle status is
@@ -20,7 +20,7 @@ environment variables, and optionally a `.env` file in the working directory via
20
20
 
21
21
  ## Routes
22
22
 
23
- Load the `/docs` page for a detailed view of routes (below the readme) if in debug mode.
23
+ Load the `/docs` page for a detailed view of routes (below the readme).
24
24
 
25
25
  **No-project routes**
26
26
 
@@ -48,8 +48,8 @@ Load the `/docs` page for a detailed view of routes (below the readme) if in deb
48
48
  | heads | `updates`, `pf` | `GET /project/{project}?a=heads` | List branch heads. Supports long-poll subscribe. |
49
49
  | tags | `updates`, `pf` | `GET /project/{project}?a=tags` | List all tags. Supports long-poll subscribe. |
50
50
  | tag | `h` | `GET /project/{project}?a=tag&h=...` | Single tag view (tag ref or hash). |
51
- | commit | — | `GET /project/{project}?a=commit` | Commit information. |
52
- | commitdiff | — | `GET /project/{project}?a=commitdiff` | Commit diff (unified diff rendered with [diff2html](https://github.com/rtfpessoa/diff2html)). |
51
+ | commit | `h` | `GET /project/{project}?a=commit&h=...` | Commit information. |
52
+ | commitdiff | `h` | `GET /project/{project}?a=commitdiff&h=...` | Commit diff (unified diff rendered with [diff2html](https://github.com/rtfpessoa/diff2html)). |
53
53
  | patch | `h` | `GET /project/{project}?a=patch&h=...` | Single-commit patch (plain text). |
54
54
  | patches | `h`, `hb` | `GET /project/{project}?a=patches&h=...&hb=...` | Multi-commit patches for range `hb..h` (plain text). |
55
55
  | blobdiff | `h`, `hb`, `f`, `fp` | `GET /project/{project}?a=blobdiff&h=...&hb=...&f=...&fp=...` | Blob diff (two versions of a file) rendered with diff2html. |
@@ -100,16 +100,16 @@ subscribe to the project's change queue. The request is held for up to 30 second
100
100
  just the URL project (the action is still rendered for the URL project).
101
101
 
102
102
  The notify endpoint is intended for loopback use by `pygittools` post-receive hooks (see
103
- `pygittools/hook_samples/post-receive.notify`); production deployments should restrict it
104
- at the reverse proxy.
103
+ `pygittools/hook_samples/post-receive.notify`). The server enforces loopback-only access and
104
+ rejects unknown projects; production deployments should still restrict it at the reverse proxy.
105
105
 
106
106
  ## Hook management
107
107
 
108
- The project summary page exposes an **Update Hook** row that installs / removes the
109
- `update` bundle: `post-receive.notify` (feeds the change queue when refs are pushed) and
110
- `post-commit.notify` (feeds the change queue when a working clone of this repo records
111
- a local commit). Either is sufficient to wake long-poll subscribers; installing both
112
- covers server-side and client-side commit paths.
108
+ The project summary page exposes a **Hooks** row that lists installed hooks and lets you
109
+ install or remove pygittools samples, including the `update` bundle: `post-receive.notify`
110
+ (feeds the change queue when refs are pushed) and `post-commit.notify` (feeds the change
111
+ queue when a working clone of this repo records a local commit). Either is sufficient to
112
+ wake long-poll subscribers; installing both covers server-side and client-side commit paths.
113
113
 
114
114
  The same operations are available programmatically via
115
115
  `POST /project/{name}/hook?name=<sample-or-bundle>&op=<...>`. Bundle status is
@@ -1,3 +1,4 @@
1
+ import asyncio
1
2
  import json
2
3
  import os
3
4
  import sys
@@ -6,11 +7,20 @@ from pathlib import Path
6
7
  from unittest.mock import AsyncMock, patch
7
8
 
8
9
  import pytest
10
+ from fastapi import HTTPException
9
11
  from fastapi.testclient import TestClient
10
12
 
11
13
  from pygitweb.auth import ACCESS_TOKEN_COOKIE_NAME
12
14
  from pygitweb.auth_config import AuthConfig, LocalUser, init_auth_config, write_auth_config
13
- from pygitweb.auth_oauth import OAUTH_STATE_COOKIE_NAME, _seal_oauth_state, is_oauth_email_allowed
15
+ from pygitweb.auth_oauth import (
16
+ OAUTH_STATE_COOKIE_NAME,
17
+ _exchange_code,
18
+ _fetch_github_profile,
19
+ _fetch_google_profile,
20
+ _fetch_profile,
21
+ _seal_oauth_state,
22
+ is_oauth_email_allowed,
23
+ )
14
24
  from pygitweb.config import Settings, settings
15
25
  from pygitweb.conftest import clear_client_cookies
16
26
  from pygitweb.gravatar import gravatar_url
@@ -19,6 +29,19 @@ from pygitweb.password_hash import hash_password, verify_password
19
29
  from pygitweb.permissions import PERMISSION_ADD_PROJECTS, PermissionsMap
20
30
  from pygitweb.sessions import get_session
21
31
 
32
+
33
+ class _DummyResponse:
34
+ def __init__(self, payload: object, is_success: bool = True) -> None:
35
+ self._payload = payload
36
+ self.is_success = is_success
37
+
38
+ def raise_for_status(self) -> None:
39
+ return None
40
+
41
+ def json(self) -> object:
42
+ return self._payload
43
+
44
+
22
45
  _LOCAL_ADMIN = AuthConfig(
23
46
  auth_mode="local",
24
47
  local_users=[LocalUser(user="admin", password="secret")],
@@ -388,3 +411,151 @@ def test_authenticate_local_user_pass_hash() -> None:
388
411
  assert user is not None
389
412
  assert user.username == "admin"
390
413
  assert authenticate_local_user("admin", "wrong") is None
414
+
415
+
416
+ def test_exchange_code_success(oauth_auth_config: AuthConfig) -> None:
417
+ resp = _DummyResponse({"access_token": "tok", "other": "x"})
418
+
419
+ client = AsyncMock()
420
+ client.post.return_value = resp
421
+ client.__aenter__.return_value = client
422
+ client.__aexit__.return_value = None
423
+
424
+ async def runner() -> None:
425
+ with patch("pygitweb.auth_oauth.httpx.AsyncClient", return_value=client):
426
+ out = await _exchange_code(
427
+ oauth_auth_config,
428
+ code="auth-code",
429
+ code_verifier="verifier",
430
+ )
431
+ assert out["access_token"] == "tok"
432
+
433
+ asyncio.run(runner())
434
+
435
+
436
+ def test_exchange_code_rejects_non_mapping(oauth_auth_config: AuthConfig) -> None:
437
+ resp = _DummyResponse(["not-a-dict"])
438
+
439
+ client = AsyncMock()
440
+ client.post.return_value = resp
441
+ client.__aenter__.return_value = client
442
+ client.__aexit__.return_value = None
443
+
444
+ async def runner() -> None:
445
+ with patch("pygitweb.auth_oauth.httpx.AsyncClient", return_value=client), pytest.raises(HTTPException):
446
+ await _exchange_code(
447
+ oauth_auth_config,
448
+ code="auth-code",
449
+ code_verifier="verifier",
450
+ )
451
+
452
+ asyncio.run(runner())
453
+
454
+
455
+ def test_fetch_google_profile_success(oauth_auth_config: AuthConfig) -> None:
456
+ resp = _DummyResponse({"sub": "123", "email": "user@example.com"})
457
+
458
+ client = AsyncMock()
459
+ client.get.return_value = resp
460
+ client.__aenter__.return_value = client
461
+ client.__aexit__.return_value = None
462
+
463
+ async def runner() -> None:
464
+ with patch("pygitweb.auth_oauth.httpx.AsyncClient", return_value=client):
465
+ profile = await _fetch_google_profile("access-token")
466
+ assert profile["email"] == "user@example.com"
467
+
468
+ asyncio.run(runner())
469
+
470
+
471
+ def test_fetch_google_profile_rejects_non_mapping(oauth_auth_config: AuthConfig) -> None:
472
+ resp = _DummyResponse("not-a-dict")
473
+
474
+ client = AsyncMock()
475
+ client.get.return_value = resp
476
+ client.__aenter__.return_value = client
477
+ client.__aexit__.return_value = None
478
+
479
+ async def runner() -> None:
480
+ with patch("pygitweb.auth_oauth.httpx.AsyncClient", return_value=client), pytest.raises(HTTPException):
481
+ await _fetch_google_profile("access-token")
482
+
483
+ asyncio.run(runner())
484
+
485
+
486
+ def test_fetch_github_profile_uses_primary_verified_email() -> None:
487
+ resp_user = _DummyResponse({"login": "octocat", "email": None})
488
+ resp_emails = _DummyResponse([
489
+ {"email": "secondary@example.com", "primary": False, "verified": True},
490
+ {"email": "primary@example.com", "primary": True, "verified": True},
491
+ ])
492
+
493
+ client = AsyncMock()
494
+ client.get.side_effect = (resp_user, resp_emails)
495
+ client.__aenter__.return_value = client
496
+ client.__aexit__.return_value = None
497
+
498
+ async def runner() -> None:
499
+ with patch("pygitweb.auth_oauth.httpx.AsyncClient", return_value=client):
500
+ profile = await _fetch_github_profile("access-token")
501
+ assert profile["email"] == "primary@example.com"
502
+
503
+ asyncio.run(runner())
504
+
505
+
506
+ def test_fetch_github_profile_rejects_non_mapping() -> None:
507
+ resp_user = _DummyResponse("not-a-dict")
508
+
509
+ client = AsyncMock()
510
+ client.get.return_value = resp_user
511
+ client.__aenter__.return_value = client
512
+ client.__aexit__.return_value = None
513
+
514
+ async def runner() -> None:
515
+ with patch("pygitweb.auth_oauth.httpx.AsyncClient", return_value=client), pytest.raises(HTTPException):
516
+ await _fetch_github_profile("access-token")
517
+
518
+ asyncio.run(runner())
519
+
520
+
521
+ def test_fetch_profile_google(oauth_auth_config: AuthConfig) -> None:
522
+ token_payload = {"access_token": "tok"}
523
+ profile: dict[str, object] = {"sub": "123"}
524
+
525
+ async def runner() -> None:
526
+ with patch("pygitweb.auth_oauth._fetch_google_profile", new_callable=AsyncMock, return_value=profile):
527
+ out = await _fetch_profile(oauth_auth_config, token_payload)
528
+ assert out is profile
529
+
530
+ asyncio.run(runner())
531
+
532
+
533
+ def test_fetch_profile_github() -> None:
534
+ cfg = _OAUTH_CFG.model_copy(update={"oauth_provider": "github"})
535
+ token_payload = {"access_token": "tok"}
536
+ profile: dict[str, object] = {"id": "123"}
537
+
538
+ async def runner() -> None:
539
+ with patch("pygitweb.auth_oauth._fetch_github_profile", new_callable=AsyncMock, return_value=profile):
540
+ out = await _fetch_profile(cfg, token_payload)
541
+ assert out is profile
542
+
543
+ asyncio.run(runner())
544
+
545
+
546
+ def test_fetch_profile_missing_access_token(oauth_auth_config: AuthConfig) -> None:
547
+ async def runner() -> None:
548
+ with pytest.raises(HTTPException):
549
+ await _fetch_profile(oauth_auth_config, {})
550
+
551
+ asyncio.run(runner())
552
+
553
+
554
+ def test_fetch_profile_unknown_provider() -> None:
555
+ cfg = _OAUTH_CFG.model_copy(update={"oauth_provider": "other"})
556
+
557
+ async def runner() -> None:
558
+ with pytest.raises(HTTPException):
559
+ await _fetch_profile(cfg, {"access_token": "tok"})
560
+
561
+ asyncio.run(runner())
@@ -10,6 +10,7 @@ import os
10
10
  from collections.abc import AsyncGenerator
11
11
  from contextlib import asynccontextmanager
12
12
  from datetime import UTC, datetime
13
+ from importlib.resources import files
13
14
  from pathlib import Path
14
15
  from typing import Annotated, Literal
15
16
  from urllib.parse import quote, urlencode
@@ -163,8 +164,7 @@ async def app_lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
163
164
  clear_all_sessions()
164
165
 
165
166
 
166
- with open("pygitweb/README.md", encoding="utf-8") as _readme_file:
167
- _app_description = _readme_file.read()
167
+ _app_description = (files("pygitweb") / "README.md").read_text(encoding="utf-8")
168
168
 
169
169
  app = FastAPI(
170
170
  debug=(settings.AUTH is False),
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pygitweb
3
- Version: 0.2.0
3
+ Version: 0.2.1
4
4
  Summary: Gitweb reimplementation with FastAPI and Pygit2
5
5
  License-Expression: MIT
6
6
  Requires-Python: >=3.11
@@ -40,7 +40,7 @@ environment variables, and optionally a `.env` file in the working directory via
40
40
 
41
41
  ## Routes
42
42
 
43
- Load the `/docs` page for a detailed view of routes (below the readme) if in debug mode.
43
+ Load the `/docs` page for a detailed view of routes (below the readme).
44
44
 
45
45
  **No-project routes**
46
46
 
@@ -68,8 +68,8 @@ Load the `/docs` page for a detailed view of routes (below the readme) if in deb
68
68
  | heads | `updates`, `pf` | `GET /project/{project}?a=heads` | List branch heads. Supports long-poll subscribe. |
69
69
  | tags | `updates`, `pf` | `GET /project/{project}?a=tags` | List all tags. Supports long-poll subscribe. |
70
70
  | tag | `h` | `GET /project/{project}?a=tag&h=...` | Single tag view (tag ref or hash). |
71
- | commit | — | `GET /project/{project}?a=commit` | Commit information. |
72
- | commitdiff | — | `GET /project/{project}?a=commitdiff` | Commit diff (unified diff rendered with [diff2html](https://github.com/rtfpessoa/diff2html)). |
71
+ | commit | `h` | `GET /project/{project}?a=commit&h=...` | Commit information. |
72
+ | commitdiff | `h` | `GET /project/{project}?a=commitdiff&h=...` | Commit diff (unified diff rendered with [diff2html](https://github.com/rtfpessoa/diff2html)). |
73
73
  | patch | `h` | `GET /project/{project}?a=patch&h=...` | Single-commit patch (plain text). |
74
74
  | patches | `h`, `hb` | `GET /project/{project}?a=patches&h=...&hb=...` | Multi-commit patches for range `hb..h` (plain text). |
75
75
  | blobdiff | `h`, `hb`, `f`, `fp` | `GET /project/{project}?a=blobdiff&h=...&hb=...&f=...&fp=...` | Blob diff (two versions of a file) rendered with diff2html. |
@@ -120,16 +120,16 @@ subscribe to the project's change queue. The request is held for up to 30 second
120
120
  just the URL project (the action is still rendered for the URL project).
121
121
 
122
122
  The notify endpoint is intended for loopback use by `pygittools` post-receive hooks (see
123
- `pygittools/hook_samples/post-receive.notify`); production deployments should restrict it
124
- at the reverse proxy.
123
+ `pygittools/hook_samples/post-receive.notify`). The server enforces loopback-only access and
124
+ rejects unknown projects; production deployments should still restrict it at the reverse proxy.
125
125
 
126
126
  ## Hook management
127
127
 
128
- The project summary page exposes an **Update Hook** row that installs / removes the
129
- `update` bundle: `post-receive.notify` (feeds the change queue when refs are pushed) and
130
- `post-commit.notify` (feeds the change queue when a working clone of this repo records
131
- a local commit). Either is sufficient to wake long-poll subscribers; installing both
132
- covers server-side and client-side commit paths.
128
+ The project summary page exposes a **Hooks** row that lists installed hooks and lets you
129
+ install or remove pygittools samples, including the `update` bundle: `post-receive.notify`
130
+ (feeds the change queue when refs are pushed) and `post-commit.notify` (feeds the change
131
+ queue when a working clone of this repo records a local commit). Either is sufficient to
132
+ wake long-poll subscribers; installing both covers server-side and client-side commit paths.
133
133
 
134
134
  The same operations are available programmatically via
135
135
  `POST /project/{name}/hook?name=<sample-or-bundle>&op=<...>`. Bundle status is
@@ -57,6 +57,7 @@ templates_env.py
57
57
  timeline_cache.py
58
58
  validation.py
59
59
  validation_test.py
60
+ ./README.md
60
61
  ./__init__.py
61
62
  ./__meta__.py
62
63
  ./actions.py
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "pygitweb"
7
- version = "0.2.0"
7
+ version = "0.2.1"
8
8
  description = "Gitweb reimplementation with FastAPI and Pygit2"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -30,7 +30,7 @@ packages = ["pygitweb", "pygitweb.api", "pygitweb.api.plugins"]
30
30
  pygitweb = "."
31
31
 
32
32
  [tool.setuptools.package-data]
33
- pygitweb = ["templates/**/*", "static/**/*", "*.schema.json"]
33
+ pygitweb = ["README.md", "templates/**/*", "static/**/*", "*.schema.json"]
34
34
 
35
35
  [tool.uv.sources]
36
36
  pygittools = { workspace = true }
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes