pygitweb 0.1.0__py3-none-any.whl

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 (95) hide show
  1. pygitweb/__init__.py +22 -0
  2. pygitweb/__meta__.py +9 -0
  3. pygitweb/actions.py +1303 -0
  4. pygitweb/addproject_permissions_test.py +114 -0
  5. pygitweb/api/__init__.py +0 -0
  6. pygitweb/api/action.py +20 -0
  7. pygitweb/api/plugins/__init__.py +0 -0
  8. pygitweb/api/plugins/project_zip.py +71 -0
  9. pygitweb/api/project.py +67 -0
  10. pygitweb/api/subpage.py +22 -0
  11. pygitweb/auth.py +387 -0
  12. pygitweb/auth.schema.json +106 -0
  13. pygitweb/auth_config.py +243 -0
  14. pygitweb/auth_oauth.py +343 -0
  15. pygitweb/auth_test.py +390 -0
  16. pygitweb/boards_permissions_test.py +113 -0
  17. pygitweb/change_queue.py +110 -0
  18. pygitweb/change_queue_routes_test.py +146 -0
  19. pygitweb/change_queue_test.py +91 -0
  20. pygitweb/comments_permissions_test.py +140 -0
  21. pygitweb/config.py +185 -0
  22. pygitweb/conftest.py +43 -0
  23. pygitweb/dependencies.py +87 -0
  24. pygitweb/formatting.py +182 -0
  25. pygitweb/formatting_test.py +68 -0
  26. pygitweb/git_helpers.py +454 -0
  27. pygitweb/git_helpers_test.py +223 -0
  28. pygitweb/gravatar.py +16 -0
  29. pygitweb/gravatar_test.py +10 -0
  30. pygitweb/hooks_install.py +278 -0
  31. pygitweb/hooks_install_test.py +363 -0
  32. pygitweb/hooks_permissions_test.py +116 -0
  33. pygitweb/main.py +887 -0
  34. pygitweb/merge_requests.py +230 -0
  35. pygitweb/merge_requests_test.py +85 -0
  36. pygitweb/mr_permissions_test.py +148 -0
  37. pygitweb/password_hash.py +74 -0
  38. pygitweb/password_hash_test.py +20 -0
  39. pygitweb/permissions.py +209 -0
  40. pygitweb/permissions_test.py +86 -0
  41. pygitweb/plugin_loader.py +83 -0
  42. pygitweb/projects.py +213 -0
  43. pygitweb/projects_test.py +186 -0
  44. pygitweb/sessions.py +73 -0
  45. pygitweb/settings.py +412 -0
  46. pygitweb/settings.schema.json +73 -0
  47. pygitweb/settings_config.py +122 -0
  48. pygitweb/settings_config_test.py +101 -0
  49. pygitweb/settings_permissions_test.py +76 -0
  50. pygitweb/static/blob-view.js +133 -0
  51. pygitweb/static/board.js +839 -0
  52. pygitweb/static/hook-install.js +53 -0
  53. pygitweb/static/icons/cloud-down.svg +1 -0
  54. pygitweb/static/icons/cloud-exclamation.svg +1 -0
  55. pygitweb/static/icons/cloud-filled.svg +1 -0
  56. pygitweb/static/icons/cloud-storm.svg +1 -0
  57. pygitweb/static/icons/cloud-up.svg +1 -0
  58. pygitweb/static/icons/cloud.svg +1 -0
  59. pygitweb/static/logo.svg +44 -0
  60. pygitweb/static/main.css +1210 -0
  61. pygitweb/static/navbar-auth.js +82 -0
  62. pygitweb/static/readme-render.js +174 -0
  63. pygitweb/static/search-page.js +139 -0
  64. pygitweb/static/summary-ref-switcher.js +130 -0
  65. pygitweb/static/themes/solarized.css +34 -0
  66. pygitweb/tasks.py +595 -0
  67. pygitweb/tasks_permissions_test.py +137 -0
  68. pygitweb/tasks_routes_test.py +430 -0
  69. pygitweb/templates/addproject.html +174 -0
  70. pygitweb/templates/blobdiff.html +57 -0
  71. pygitweb/templates/board.html +31 -0
  72. pygitweb/templates/board_card.html +44 -0
  73. pygitweb/templates/commitdiff.html +57 -0
  74. pygitweb/templates/hooks_summary_cell.html +37 -0
  75. pygitweb/templates/login.html +37 -0
  76. pygitweb/templates/merge_request_summary_cell.html +18 -0
  77. pygitweb/templates/merge_request_tag.html +90 -0
  78. pygitweb/templates/pagination.html +44 -0
  79. pygitweb/templates/preamble.html +89 -0
  80. pygitweb/templates/project_settings.html +13 -0
  81. pygitweb/templates/pygit2_settings.html +12 -0
  82. pygitweb/templates/pygitweb_settings.html +12 -0
  83. pygitweb/templates/search.html +77 -0
  84. pygitweb/templates/settings_form_field.html +27 -0
  85. pygitweb/templates/table.html +15 -0
  86. pygitweb/templates/user_profile.html +10 -0
  87. pygitweb/templates_env.py +21 -0
  88. pygitweb/timeline_cache.py +243 -0
  89. pygitweb/validation.py +75 -0
  90. pygitweb/validation_test.py +167 -0
  91. pygitweb-0.1.0.dist-info/METADATA +165 -0
  92. pygitweb-0.1.0.dist-info/RECORD +95 -0
  93. pygitweb-0.1.0.dist-info/WHEEL +5 -0
  94. pygitweb-0.1.0.dist-info/licenses/LICENSE +21 -0
  95. pygitweb-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,167 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ import stat
5
+ import sys
6
+ from collections.abc import Generator
7
+ from pathlib import Path
8
+
9
+ import pygit2
10
+ import pytest
11
+
12
+ from pygitweb.validation import check_export_ok, is_valid_pathname, is_valid_project, is_valid_ref_format
13
+
14
+
15
+ class TestValidation:
16
+ @pytest.fixture(scope="class")
17
+ def project_env(self, tmp_path_factory: pytest.TempPathFactory) -> Generator[dict[str, str], None, None]:
18
+ projectroot: Path = tmp_path_factory.mktemp("validation-projects")
19
+ valid_dir: Path = projectroot / "valid"
20
+ invalid_dir: Path = projectroot / "invalid"
21
+ unreachable_dir: Path = projectroot / "unreachable"
22
+
23
+ valid_dir.mkdir()
24
+ invalid_dir.mkdir()
25
+ unreachable_dir.mkdir()
26
+
27
+ pygit2.init_repository(str(valid_dir), bare=True)
28
+ (valid_dir / "git-daemon-export-ok").write_text("", encoding="utf-8")
29
+
30
+ pygit2.init_repository(str(unreachable_dir), bare=True)
31
+ (unreachable_dir / "git-daemon-export-ok").write_text("", encoding="utf-8")
32
+ original_mode: int = stat.S_IMODE(unreachable_dir.stat().st_mode)
33
+ os.chmod(unreachable_dir, 0)
34
+
35
+ try:
36
+ yield {
37
+ "projectroot": str(projectroot),
38
+ "valid_name": "valid",
39
+ "invalid_name": "invalid",
40
+ "unreachable_name": "unreachable",
41
+ "valid_dir": str(valid_dir),
42
+ "invalid_dir": str(invalid_dir),
43
+ "unreachable_dir": str(unreachable_dir),
44
+ "export_ok": "git-daemon-export-ok",
45
+ }
46
+ finally:
47
+ os.chmod(unreachable_dir, original_mode)
48
+
49
+ class TestPathname:
50
+ def test_is_valid_pathname_success(self) -> None:
51
+ assert is_valid_pathname("group/project.git") is True
52
+
53
+ @pytest.mark.parametrize("input_path", [None, "a//b", "a/./b", "a/../b", "a\0b"])
54
+ def test_is_valid_pathname_failure_modes(self, input_path: str | None) -> None:
55
+ assert is_valid_pathname(input_path) is False
56
+
57
+ class TestRefFormat:
58
+ def test_is_valid_ref_format_success(self) -> None:
59
+ assert is_valid_ref_format("refs/heads/main") is True
60
+
61
+ @pytest.mark.parametrize(
62
+ "input_ref",
63
+ [None, ".bad", "refs/heads/../x", "refs/heads/x.lock", "refs/heads/x ", "refs/heads/x?y"],
64
+ )
65
+ def test_is_valid_ref_format_failure_modes(self, input_ref: str | None) -> None:
66
+ assert is_valid_ref_format(input_ref) is False
67
+
68
+ class TestCheckExportOk:
69
+ def test_check_export_ok_success(self, project_env: dict[str, str]) -> None:
70
+ assert check_export_ok(project_env["valid_dir"], project_env["export_ok"]) is True
71
+
72
+ def test_check_export_ok_fails_for_missing_directory(self, project_env: dict[str, str]) -> None:
73
+ missing_dir: str = str(Path(project_env["projectroot"]) / "missing")
74
+ assert check_export_ok(missing_dir, project_env["export_ok"]) is False
75
+
76
+ def test_check_export_ok_fails_for_non_repo_directory(self, project_env: dict[str, str]) -> None:
77
+ assert check_export_ok(project_env["invalid_dir"], project_env["export_ok"]) is False
78
+
79
+ def test_check_export_ok_fails_for_missing_export_file(self, project_env: dict[str, str]) -> None:
80
+ assert check_export_ok(project_env["valid_dir"], "missing-export-ok") is False
81
+
82
+ def test_check_export_ok_fails_when_hook_rejects(self, project_env: dict[str, str]) -> None:
83
+ assert (
84
+ check_export_ok(
85
+ project_env["valid_dir"],
86
+ project_env["export_ok"],
87
+ export_auth_hook=lambda _path: False,
88
+ )
89
+ is False
90
+ )
91
+
92
+ @pytest.mark.skipif(sys.platform == "win32", reason="chmod-based unreachable directory check is not portable")
93
+ def test_check_export_ok_fails_for_unreachable_repo(self, project_env: dict[str, str]) -> None:
94
+ assert check_export_ok(project_env["unreachable_dir"], project_env["export_ok"]) is False
95
+
96
+ class TestValidProject:
97
+ def test_is_valid_project_success(self, project_env: dict[str, str]) -> None:
98
+ assert (
99
+ is_valid_project(
100
+ project=project_env["valid_name"],
101
+ projectroot=project_env["projectroot"],
102
+ export_ok=project_env["export_ok"],
103
+ strict_export=False,
104
+ project_in_list=lambda _project: True,
105
+ )
106
+ is True
107
+ )
108
+
109
+ def test_is_valid_project_fails_for_none(self, project_env: dict[str, str]) -> None:
110
+ assert (
111
+ is_valid_project(
112
+ project=None,
113
+ projectroot=project_env["projectroot"],
114
+ export_ok=project_env["export_ok"],
115
+ strict_export=False,
116
+ project_in_list=lambda _project: True,
117
+ )
118
+ is False
119
+ )
120
+
121
+ def test_is_valid_project_fails_for_invalid_pathname(self, project_env: dict[str, str]) -> None:
122
+ assert (
123
+ is_valid_project(
124
+ project="bad/../name",
125
+ projectroot=project_env["projectroot"],
126
+ export_ok=project_env["export_ok"],
127
+ strict_export=False,
128
+ project_in_list=lambda _project: True,
129
+ )
130
+ is False
131
+ )
132
+
133
+ def test_is_valid_project_fails_for_missing_directory(self, project_env: dict[str, str]) -> None:
134
+ assert (
135
+ is_valid_project(
136
+ project="missing",
137
+ projectroot=project_env["projectroot"],
138
+ export_ok=project_env["export_ok"],
139
+ strict_export=False,
140
+ project_in_list=lambda _project: True,
141
+ )
142
+ is False
143
+ )
144
+
145
+ def test_is_valid_project_fails_when_export_check_fails(self, project_env: dict[str, str]) -> None:
146
+ assert (
147
+ is_valid_project(
148
+ project=project_env["invalid_name"],
149
+ projectroot=project_env["projectroot"],
150
+ export_ok=project_env["export_ok"],
151
+ strict_export=False,
152
+ project_in_list=lambda _project: True,
153
+ )
154
+ is False
155
+ )
156
+
157
+ def test_is_valid_project_fails_strict_export_when_not_listed(self, project_env: dict[str, str]) -> None:
158
+ assert (
159
+ is_valid_project(
160
+ project=project_env["valid_name"],
161
+ projectroot=project_env["projectroot"],
162
+ export_ok=project_env["export_ok"],
163
+ strict_export=True,
164
+ project_in_list=lambda _project: False,
165
+ )
166
+ is False
167
+ )
@@ -0,0 +1,165 @@
1
+ Metadata-Version: 2.4
2
+ Name: pygitweb
3
+ Version: 0.1.0
4
+ Summary: Gitweb reimplementation with FastAPI and Pygit2
5
+ License-Expression: MIT
6
+ Requires-Python: >=3.11
7
+ Description-Content-Type: text/markdown
8
+ License-File: LICENSE
9
+ Requires-Dist: pygittools>=0.1.0
10
+ Requires-Dist: fastapi[standard-no-fastapi-cloud-cli]>=0.104.0
11
+ Requires-Dist: uvicorn[standard]>=0.24.0
12
+ Requires-Dist: python-multipart>=0.0.6
13
+ Requires-Dist: jinja2>=3.1.0
14
+ Requires-Dist: orjson>=0.19.0
15
+ Requires-Dist: pygit2>=1.12.0
16
+ Requires-Dist: pydantic-settings>=2.13.0
17
+ Requires-Dist: httpx>=0.27.0
18
+ Requires-Dist: python-ripgrep==0.0.9
19
+ Dynamic: license-file
20
+
21
+ # Pygitweb
22
+
23
+ Gitweb reimplementation using **Python**, **FastAPI**, and **Pygit2**, ported from `git/gitweb/gitweb.perl`.
24
+
25
+ ## Setup
26
+
27
+ ```bash
28
+ uv sync --package pygitweb
29
+ ```
30
+
31
+ Or install everything in the workspace (all members + dev tools):
32
+
33
+ ```bash
34
+ uv sync --all-packages --group dev
35
+ ```
36
+
37
+ ## Run
38
+
39
+ From the repository root (after `uv sync`), with the workspace `.venv` activated:
40
+
41
+ ```bash
42
+ uvicorn pygitweb.main:app --reload --host 0.0.0.0 --port 8000
43
+ ```
44
+
45
+ Or:
46
+
47
+ ```bash
48
+ python -m pygitweb.main
49
+ ```
50
+
51
+ ## Config
52
+
53
+ Settings load from `~/.pygitweb/settings.json` (or `PYGITWEB_SETTINGS_CONFIG`), `PYGITWEB_*`
54
+ environment variables, and optionally a `.env` file in the working directory via
55
+ `pydantic-settings`. See `pygitweb/config.py` and `pygitweb/settings.schema.json` for the full schema. Common ones:
56
+
57
+ - `PYGITWEB_PROJECTROOT` — absolute path to directory containing git repositories (default: `$HOME`)
58
+ - `PYGITWEB_PROJECTS_LIST` — directory to scan, or path to a project-list file (default: `PROJECTROOT`)
59
+ - `PYGITWEB_EXPORT_OK` — filename that must exist to allow export (e.g. `git-daemon-export-ok`); empty = no check
60
+ - `PYGITWEB_SITE_NAME` — site name in titles (default: `PyGitWeb`)
61
+ - `PYGITWEB_GIT` — path to the git executable (default: `git`)
62
+ - `PYGITWEB_AUTH` — `0` / `false` / `off` disables auth; `1` / `true` / `on` enables it. If **unset or empty**, auth defaults to **on**.
63
+ - `PYGITWEB_AUTH_CONFIG` — path to the auth JSON file (default: `~/.pygitweb/auth.json`). Generated to a reasonable default if not present.
64
+ - `PYGITWEB_SETTINGS_CONFIG` — path to the settings JSON file (default: `~/.pygitweb/settings.json`). Created from the environment on first run if missing.
65
+ - Browser login: `GET /login` (optional `?next=/path`), `POST /login` with form fields `username`, `password`, `next`; sets an HttpOnly cookie with an opaque in-process session id (also accepted by protected routes via `Authorization: Bearer`). Sessions are wiped when the process exits. `GET /logout?next=/` revokes the session and clears the cookie.
66
+
67
+ ## Routes
68
+
69
+ Load the `/docs` page for a detailed view of routes (below the readme) if in debug mode.
70
+
71
+ **No-project routes**
72
+
73
+ - `GET /`: project list
74
+ - `GET /index`: plain text project index (path, owner)
75
+ - `GET /opml`: OPML feed list
76
+ - `GET /project/{name}`: Project dispatch (See actions table)
77
+
78
+ **Hook management routes**
79
+
80
+ - `GET /project/{name}/hooks`: JSON `{hooks: [...samples...], bundles: [...]}` with current status (`installed`, `not_installed`, `different`) for every pygittools sample and bundle
81
+ - `POST /project/{name}/hook?name=<sample-or-bundle>&op=<add|remove|check>`: install, remove, or check one sample or bundle. `name` may be a sample filename (e.g. `post-receive.notify`) or a bundle name (e.g. `update`, which expands to `post-commit.notify` + `post-receive.notify`). `add`/`remove` refuse to clobber a custom hook with different content (returns `409`)
82
+
83
+ **Actions**
84
+
85
+ | Action | Query parameters | URL | Description |
86
+ |--------|------------------|-----|-------------|
87
+ | summary | *(default)* or `a` | `GET /project/{project}` | Project summary (description, owner, HEAD, tree link). |
88
+ | tree | `h`, `f` | `GET /project/{project}?a=tree&h=...&f=...` | Directory listing (tree). |
89
+ | blob | `h`, `f` | `GET /project/{project}?a=blob&h=...&f=...` | File view (HTML). |
90
+ | blob_plain | `h`, `f` | `GET /project/{project}?a=blob_plain&h=...&f=...` | Raw file download. |
91
+ | log | `h`, `updates`, `pf` | `GET /project/{project}?a=log&h=...` | Commit log. Supports long-poll subscribe. |
92
+ | shortlog | `h`, `updates`, `pf` | `GET /project/{project}?a=shortlog&h=...` | Shortlog. Supports long-poll subscribe. |
93
+ | history | `h`, `f`, `updates`, `pf` | `GET /project/{project}?a=history&h=...&f=...` | History of a file or path. Supports long-poll subscribe. |
94
+ | heads | `updates`, `pf` | `GET /project/{project}?a=heads` | List branch heads. Supports long-poll subscribe. |
95
+ | tags | `updates`, `pf` | `GET /project/{project}?a=tags` | List all tags. Supports long-poll subscribe. |
96
+ | tag | `h` | `GET /project/{project}?a=tag&h=...` | Single tag view (tag ref or hash). |
97
+ | commit | — | `GET /project/{project}?a=commit` | Commit information. |
98
+ | commitdiff | — | `GET /project/{project}?a=commitdiff` | Commit diff (unified diff rendered with [diff2html](https://github.com/rtfpessoa/diff2html)). |
99
+ | patch | `h` | `GET /project/{project}?a=patch&h=...` | Single-commit patch (plain text). |
100
+ | patches | `h`, `hb` | `GET /project/{project}?a=patches&h=...&hb=...` | Multi-commit patches for range `hb..h` (plain text). |
101
+ | blobdiff | `h`, `hb`, `f`, `fp` | `GET /project/{project}?a=blobdiff&h=...&hb=...&f=...&fp=...` | Blob diff (two versions of a file) rendered with diff2html. |
102
+ | blobpatch | `h`, `hb`, `f`, `fp` | `GET /project/{project}?a=blobpatch&h=...&hb=...&f=...&fp=...` | Blob diff as plain unified diff. |
103
+ | remotes | — | `GET /project/{project}?a=remotes` | List repo remotes. |
104
+ | object | `h` | `GET /project/{project}?a=object&h=...` | Show object by type (commit, tree, tag, or blob). |
105
+ | blame_raw | `h`, `f` | `GET /project/{project}?a=blame_raw&h=...&f=...` | File blame as JSON: array of `{commit_id, commit_msg, commit_author, time, line_range}`. Contiguous lines from the same commit are merged. |
106
+ | blame | `h`, `f` | `GET /project/{project}?a=blame&h=...&f=...` | File blame HTML view (syntax-highlighted source, line numbers, commit links per line range; hover shows author and date). |
107
+ | blame_incremental | — | | TODO |
108
+ | blame_data | — | | TODO |
109
+ | rss | — | | TODO |
110
+ | atom | — | | TODO |
111
+ | search | `patterns`, `paths`, `globs`, `heading`, `sort`, `max_count`, `multiline` | `GET /project/{project}?a=search&patterns=...` | Ripgrep search (via `python-ripgrep`); returns JSON. `paths` are relative to the project tree and cannot escape it. |
112
+ | search (page) | — | `GET /project/{project}/search` | Per-project search UI page. |
113
+ | search_help | — | | TODO |
114
+
115
+ ## Query parameter short names (CGI mapping)
116
+
117
+ - `p` → project
118
+ - `a` → action
119
+ - `f` → file_name
120
+ - `fp` → file_parent
121
+ - `h` → hash
122
+ - `hp` → hash_parent
123
+ - `hb` → hash_base
124
+ - `hpb` → hash_parent_base
125
+ - `pg` → page
126
+ - `o` → order
127
+ - `s` → searchtext
128
+ - `st` → searchtype
129
+ - `sf` → snapshot_format
130
+ - `opt` → extra_options
131
+ - `sr` → search_use_regexp
132
+ - `by_tag` → ctag
133
+ - `ds` → diff_style
134
+ - `pf` → project_filter
135
+ - `updates` → long-poll subscription flag (`true` parks the request up to 30s; returns `200 OK` with an empty body on timeout/shutdown, or the action data when the queue is notified)
136
+
137
+ ## Live updates (long polling)
138
+
139
+ Supported actions (`history`, `log`, `shortlog`, `heads`, `tags`) accept `updates=true` to
140
+ subscribe to the project's change queue. The request is held for up to 30 seconds:
141
+
142
+ - A `POST /_internal/notify?project=<name>` (typically from a server-side git hook) wakes
143
+ matching subscribers, who then receive the freshly-rendered action response.
144
+ - If no notification arrives in 30 seconds (or the server is shutting down), the response is `200 OK` with an empty body.
145
+ - Combine with `pf=<prefix>` to subscribe to every project under a path prefix instead of
146
+ just the URL project (the action is still rendered for the URL project).
147
+
148
+ The notify endpoint is intended for loopback use by `pygittools` post-receive hooks (see
149
+ `pygittools/hook_samples/post-receive.notify`); production deployments should restrict it
150
+ at the reverse proxy.
151
+
152
+ ## Hook management
153
+
154
+ The project summary page exposes an **Update Hook** row that installs / removes the
155
+ `update` bundle: `post-receive.notify` (feeds the change queue when refs are pushed) and
156
+ `post-commit.notify` (feeds the change queue when a working clone of this repo records
157
+ a local commit). Either is sufficient to wake long-poll subscribers; installing both
158
+ covers server-side and client-side commit paths.
159
+
160
+ The same operations are available programmatically via
161
+ `POST /project/{name}/hook?name=<sample-or-bundle>&op=<...>`. Bundle status is
162
+ `INSTALLED` only when every member is installed, `DIFFERENT` if any member's path holds
163
+ a custom hook (the bundle then refuses to install or remove anything to preserve the
164
+ custom hook), otherwise `NOT_INSTALLED`. When `PYGITWEB_AUTH` is enabled, `add` and `remove`
165
+ require a valid access token (`Authorization: Bearer …` from `POST /token`, or the cookie from `POST /login`); `check` is always allowed.
@@ -0,0 +1,95 @@
1
+ pygitweb/__init__.py,sha256=cCoIvAeErcwdpqQHH7x6G1JSkJhlXZ4pF7DwgzBoGEo,785
2
+ pygitweb/__meta__.py,sha256=k8sjJzJlBfeoLroptgmDFjC8Kud5l21cUAnivcwRWsI,240
3
+ pygitweb/actions.py,sha256=rZK59DRVXcGUqnbiUaQq6HWIzu9MUyOmqCVz6Kbu5Is,46768
4
+ pygitweb/addproject_permissions_test.py,sha256=x8xl-t4J1bOcxD32N752sOK6ZjqYEA7zY-Sj5FrXj10,3456
5
+ pygitweb/auth.py,sha256=2KZWzpvqF_4ByLK7ZVqPD8GSbHehVGw1et7tmHoK5gg,12753
6
+ pygitweb/auth.schema.json,sha256=mx7fRQFIhomYTcZ4fUSmssgstWNapfHhS1xwtgmOSQY,3120
7
+ pygitweb/auth_config.py,sha256=vKzl70k2TBxmDKwUcfZaErqXqFoEiyJpNYAqmFqBF4A,7046
8
+ pygitweb/auth_oauth.py,sha256=LTzNFxDoJL-syxkalKQyGOS8kFbkcYZ6ZRH4Egxn0Sk,11750
9
+ pygitweb/auth_test.py,sha256=2NFLvONI_4_CpVfloC8L5UFcAsy18Viv2XkiVSsFu_s,14054
10
+ pygitweb/boards_permissions_test.py,sha256=yDx3ZMiIw1EM8_-l0OlmrKlx55kFSz2tEPyWn6CB8j4,3712
11
+ pygitweb/change_queue.py,sha256=msNNlHZsRYUsnfBubc_o1B_Pybc57Mh0JD5-xTU5I4M,3463
12
+ pygitweb/change_queue_routes_test.py,sha256=N59cFuxYriICcRJghVcXLcpNCED9NFycRV8xPMzfwBQ,4897
13
+ pygitweb/change_queue_test.py,sha256=jdrELtpozFl8VLeKiDSc-C3vPBXsPgV37wCO9No8iGk,3081
14
+ pygitweb/comments_permissions_test.py,sha256=zdWGc8Cwkz7KErKCWuNIlf0xO_JZLJixj2qz2rPp5bk,4367
15
+ pygitweb/config.py,sha256=hUWKem3PGLWcj82uGpwOKWhboJ7MMvpfQw3860eKCC8,4301
16
+ pygitweb/conftest.py,sha256=qVvI7DoEVgLV_MBNxmRH3DUg3VibSFZ8qPqPdzgRVbg,1233
17
+ pygitweb/dependencies.py,sha256=SRjsa9tNp0yDlj2I2Q0Jd4Y0hLHenTKre73-a9cCaXY,2987
18
+ pygitweb/formatting.py,sha256=DWZWHtH9Z_Si-8bqUPtnrgFdD8Ne_hBxPyGyUtSL0S8,4539
19
+ pygitweb/formatting_test.py,sha256=bfoLqCiOM-vvQYOydjgZaNBmYTYf74pMXEU6uBBnkEA,2181
20
+ pygitweb/git_helpers.py,sha256=ZYWfC3O5IKqqsX-5r5DaYhsdnVMjSU7Do5Og5wrAlDs,14565
21
+ pygitweb/git_helpers_test.py,sha256=GtmEYrBtn9Ek_GIxiZtX3CApRqHc0Q02muG7PJxoXzw,8385
22
+ pygitweb/gravatar.py,sha256=H66Y8v0riP4QqijkBHjbIgHxh8uDEsY_u2Y0dYUxGgY,541
23
+ pygitweb/gravatar_test.py,sha256=71DSdZ5cnijKkpJX4tkmL25wsvmY-4zytpWOMWIXsGY,364
24
+ pygitweb/hooks_install.py,sha256=wpwwvyxVZA1_p3BuEaaVRaaXl53KhLpL21SKu6y445Q,8784
25
+ pygitweb/hooks_install_test.py,sha256=AHra4Mj7rkB8emIHf4ciY8gtaDFmbfYs-6qPVZyUUw4,15097
26
+ pygitweb/hooks_permissions_test.py,sha256=4dP4k1WEGK4ku8fi9QGRv9Ej6yvYrrcJRWC0l4kY1AA,3834
27
+ pygitweb/main.py,sha256=vOFGYptmXrvEFS3uzkL-3bZHLtI_kUBur6hn02o6irg,30091
28
+ pygitweb/merge_requests.py,sha256=3lkfGBQyW01v7zUB-37kzipqoVbfFFiibu7o6VbHv3g,7930
29
+ pygitweb/merge_requests_test.py,sha256=i_wOGWerEuN-N0JOISfwG2vrP6Gwt0RNacLeAjOeowI,2644
30
+ pygitweb/mr_permissions_test.py,sha256=S3Di7K173Zj790FiH6DBeQ8aK7ADqh3j-7gQt8Y8rG0,4317
31
+ pygitweb/password_hash.py,sha256=0qqbx1D3igOI8gLArRBfUhlfKfIHF0IivDz0-KQXBpQ,2126
32
+ pygitweb/password_hash_test.py,sha256=Kt5toLDgPhBk29-nj5IknzdBEbm7QZqfigfzr8TnDLA,745
33
+ pygitweb/permissions.py,sha256=wCDb6NmAYWbAvGIUGzcrvubVEUazah2zuyQkR2CRFSA,6262
34
+ pygitweb/permissions_test.py,sha256=EnTZZIaK6HRrLY8q257PJIdILliNo5rWHPBytRLsPEo,3017
35
+ pygitweb/plugin_loader.py,sha256=z0F1YKzvBKKSwtUjQdkDqlwMrvg0NToHDNSJR1MYmcU,2261
36
+ pygitweb/projects.py,sha256=AXJnjNkEIB8k1plgVW0Z7QqOtji_Dh8BHRcKBVWHc6M,6398
37
+ pygitweb/projects_test.py,sha256=O4sAHHYZlwnW3JcGnJPZ-8y3GI6J1Z2qHuR5kqCk7B8,7019
38
+ pygitweb/sessions.py,sha256=ImYsTPsFn-KQT3x0VH2FYY0NmYDz2lS2nfdpzn_Fzb0,1341
39
+ pygitweb/settings.py,sha256=-cmhLox34nL8z0NhN-VdlzgMXXso5CjLGd7zgqIRm7I,14054
40
+ pygitweb/settings.schema.json,sha256=4JpP4nh-drjrWWOkx5ugG6Omm-x1Ur0hiP9vM4y339s,2164
41
+ pygitweb/settings_config.py,sha256=JQhqhk489rTthWGVWJyrigkn-g_nyahal7occVFWdeU,3151
42
+ pygitweb/settings_config_test.py,sha256=7YUQZf7uWbtCNHrSOGuJo5_IQdaLAy-kXNJ838J3UAQ,3106
43
+ pygitweb/settings_permissions_test.py,sha256=sgmefSeivqaZtll2LYFp__uO1XzBM6ECT2305OSDbP0,2128
44
+ pygitweb/tasks.py,sha256=GMxtgqugKoBnuOe05aqrKWhrerDkSCRPfwMbeGTsoRw,18736
45
+ pygitweb/tasks_permissions_test.py,sha256=YcHwJWu_QQowc6437Usp05PzaKsxpq1HB0n-XE_3UIk,4172
46
+ pygitweb/tasks_routes_test.py,sha256=KWAu3cOZJoRkp-mVv5Qzdep3YpoagTtrFXUX-qYcq3s,14217
47
+ pygitweb/templates_env.py,sha256=_G13WnMEReEsjkXH1lLNI9iAgYLG1wVZnG_ZV6CQKBk,628
48
+ pygitweb/timeline_cache.py,sha256=otzk1HdPJWglxup-IUOveAfs5Pwdqd38qL3s3W9d9nw,7230
49
+ pygitweb/validation.py,sha256=EBbINv9KGvr7IDv1ERvGfzMdNBnf765SxW0tnf_dC78,2343
50
+ pygitweb/validation_test.py,sha256=di-Z-TMhu63J9jfaB4HpIegfUsGL_ixTxqVEdy46F9Q,5656
51
+ pygitweb/api/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
52
+ pygitweb/api/action.py,sha256=lDUs_YrnJ2fEbqPKhTLQNu5NOuORMOqklPbrZ5hHodU,456
53
+ pygitweb/api/project.py,sha256=VxN0-dl44QIBkDyIW45LgOUca1clm4NsJ_Li8HBtOAA,1953
54
+ pygitweb/api/subpage.py,sha256=ZJP0I-FdIFFAglC6tDSiFg3eI-p3N3bqFvn2SEudpe8,537
55
+ pygitweb/api/plugins/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
56
+ pygitweb/api/plugins/project_zip.py,sha256=HL6_8zKNTtx2knvF7AgPC1F74WTtQuVNRFMoWKpTWl0,2292
57
+ pygitweb/static/blob-view.js,sha256=A7ua6Gi63mOYTXqLJmZkTUSC-I_isirsWlMXVZGY7Qo,4621
58
+ pygitweb/static/board.js,sha256=ODTra7lS8BfwRpZkvT1v1LJTIFxmssbdyrWgvqF8jFI,31839
59
+ pygitweb/static/hook-install.js,sha256=9fd7P1K4Wi4DivCyDGUbY2jn544rFJ349pi-4fb7Yrw,1526
60
+ pygitweb/static/logo.svg,sha256=BZngZLFMkPB9gc0CayH995fi97Aq-UkEUa2mx40LpCQ,3727
61
+ pygitweb/static/main.css,sha256=eBPn6_xLx-vFDSMd-0g_LOyxe26avv-papfrNqnWv2M,24552
62
+ pygitweb/static/navbar-auth.js,sha256=XZnRsve9I8HNKI1tvUlYHsT3xb-nmOKygvXvN3MT4rw,3166
63
+ pygitweb/static/readme-render.js,sha256=KfkMCtzX2Y1cgJNNmuhPCPstmNJBQdPCJpare1iF6a0,6744
64
+ pygitweb/static/search-page.js,sha256=12ZMUuNQ1BZCydkFeSZVwOSDBF3YSIan99ziz8kiv94,4607
65
+ pygitweb/static/summary-ref-switcher.js,sha256=g8DQQJf0AlbiFJUkcuoBW1d-wLayAmIB2pWV7rOCZEQ,3981
66
+ pygitweb/static/icons/cloud-down.svg,sha256=Nj1ELY801isFYkM_Z7MzhnSD0MbulLo1WAcWnLwy2f0,597
67
+ pygitweb/static/icons/cloud-exclamation.svg,sha256=OUWHfm2tpyBX23wsWMCyFGZjd_T5qHK02uBV2gZfIAw,600
68
+ pygitweb/static/icons/cloud-filled.svg,sha256=DzIWWtyhEbznnArivAN6_1gh7yyuRqNFR0fo-4ofIdU,595
69
+ pygitweb/static/icons/cloud-storm.svg,sha256=xySNYx3INpyOYj2U7wqdEgYf_W5ETVA89QP5c1nDC6o,418
70
+ pygitweb/static/icons/cloud-up.svg,sha256=PVwC-hVAZnRN_UWZW7vA9Xh1nwmWiM_2mkXYEtPEr9c,597
71
+ pygitweb/static/icons/cloud.svg,sha256=zvEDMeAvCQhVkFq3ZVZSqTLsWZjEAtTLTMLt9Xqg9Zk,574
72
+ pygitweb/static/themes/solarized.css,sha256=GGwWWcKTzM6nsh7SGnXgOV1TiRUTQ-USaQBz5LSXaXE,669
73
+ pygitweb/templates/addproject.html,sha256=0L4yC_jxcn9NRzdJBkCx2kO-VfYHtyTNw89pKosDXVM,6438
74
+ pygitweb/templates/blobdiff.html,sha256=ITxU29VwsOgMdiCBb0wOFmZtNpwkVCXaxid8uI1CPxE,1954
75
+ pygitweb/templates/board.html,sha256=MiiFuEo6XvPWHXlBcXPvQXiqIgPtAxyCbLR8iIum3dc,1292
76
+ pygitweb/templates/board_card.html,sha256=qeh6iZzBsIFsGD4iTnzHslg1iSMNAFKiPVlk1aQgUzw,4041
77
+ pygitweb/templates/commitdiff.html,sha256=8r_R8puhqGX98Q7gWhkN2WOuhu0Sg6igCBK6d7cdDCE,1956
78
+ pygitweb/templates/hooks_summary_cell.html,sha256=WrvZlAaUcF4sbLbL1nHksQ6EKCX6Q7PeabnOA51zjMg,1601
79
+ pygitweb/templates/login.html,sha256=otRfiZRh1YSJYlSpvzNL3CXboOTRuLo_K4tFWrlEcHk,1551
80
+ pygitweb/templates/merge_request_summary_cell.html,sha256=x9mir4i9Tl4rw1K6m5BngxtFUXIQpWq5XJSShk4SPC8,1198
81
+ pygitweb/templates/merge_request_tag.html,sha256=11wv3WGSyxPui7-BoaG0woKrooQFlqB4jhrZQqYRt6Y,3642
82
+ pygitweb/templates/pagination.html,sha256=AXLY8im0gJIY2FljKeLcr6QSbppcj4QoU1g7oeW2las,1836
83
+ pygitweb/templates/preamble.html,sha256=TmH1r103N9nPhUVO3TESbxPs-gHoQZ2YzvpSmrqTO9k,8449
84
+ pygitweb/templates/project_settings.html,sha256=AdI25nGcVD5XmY6poWHmUORO9X2bC-zWtVmiZ1_Vivs,523
85
+ pygitweb/templates/pygit2_settings.html,sha256=YW8f8iqcz7iOEDF0x1LK-jpWDPNexnGbgQ1V97xGJB8,416
86
+ pygitweb/templates/pygitweb_settings.html,sha256=cJKQ1yRYFjCXg2DAIpuWnkYtIh5m3nRYyI3D3Mp75Io,420
87
+ pygitweb/templates/search.html,sha256=Xl3xu7MrpzCPREi8xeqbYEMQ1KC0T1r_LibYDZo96Hk,3346
88
+ pygitweb/templates/settings_form_field.html,sha256=47xxEqLkKiVxF1p5u-VN9JUGmjl0krXhjzBcbZfY8LQ,1331
89
+ pygitweb/templates/table.html,sha256=dCOuoHsOKKVH66Ny77677peeUE-OEXCScZRC0e0H738,258
90
+ pygitweb/templates/user_profile.html,sha256=KUcMt9W8AFydUPlrE7jx_PmftSVadRy9lJzzvT-vgeY,309
91
+ pygitweb-0.1.0.dist-info/licenses/LICENSE,sha256=oWN3hgCPpp69WEgHXOML1NEDCYdp6SRmvMTEl0XK9ko,1068
92
+ pygitweb-0.1.0.dist-info/METADATA,sha256=PlqjaMUHKC43MHgjs4_6SDp1PWI_JXvXrGN0RB55j6k,9025
93
+ pygitweb-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
94
+ pygitweb-0.1.0.dist-info/top_level.txt,sha256=lCXrW6CHwXIrR8Kp5KW10jJ5kb2kgHzlc7Y7FsTQD7g,9
95
+ pygitweb-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Will Bowers
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ pygitweb