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.
- pygitweb/__init__.py +22 -0
- pygitweb/__meta__.py +9 -0
- pygitweb/actions.py +1303 -0
- pygitweb/addproject_permissions_test.py +114 -0
- pygitweb/api/__init__.py +0 -0
- pygitweb/api/action.py +20 -0
- pygitweb/api/plugins/__init__.py +0 -0
- pygitweb/api/plugins/project_zip.py +71 -0
- pygitweb/api/project.py +67 -0
- pygitweb/api/subpage.py +22 -0
- pygitweb/auth.py +387 -0
- pygitweb/auth.schema.json +106 -0
- pygitweb/auth_config.py +243 -0
- pygitweb/auth_oauth.py +343 -0
- pygitweb/auth_test.py +390 -0
- pygitweb/boards_permissions_test.py +113 -0
- pygitweb/change_queue.py +110 -0
- pygitweb/change_queue_routes_test.py +146 -0
- pygitweb/change_queue_test.py +91 -0
- pygitweb/comments_permissions_test.py +140 -0
- pygitweb/config.py +185 -0
- pygitweb/conftest.py +43 -0
- pygitweb/dependencies.py +87 -0
- pygitweb/formatting.py +182 -0
- pygitweb/formatting_test.py +68 -0
- pygitweb/git_helpers.py +454 -0
- pygitweb/git_helpers_test.py +223 -0
- pygitweb/gravatar.py +16 -0
- pygitweb/gravatar_test.py +10 -0
- pygitweb/hooks_install.py +278 -0
- pygitweb/hooks_install_test.py +363 -0
- pygitweb/hooks_permissions_test.py +116 -0
- pygitweb/main.py +887 -0
- pygitweb/merge_requests.py +230 -0
- pygitweb/merge_requests_test.py +85 -0
- pygitweb/mr_permissions_test.py +148 -0
- pygitweb/password_hash.py +74 -0
- pygitweb/password_hash_test.py +20 -0
- pygitweb/permissions.py +209 -0
- pygitweb/permissions_test.py +86 -0
- pygitweb/plugin_loader.py +83 -0
- pygitweb/projects.py +213 -0
- pygitweb/projects_test.py +186 -0
- pygitweb/sessions.py +73 -0
- pygitweb/settings.py +412 -0
- pygitweb/settings.schema.json +73 -0
- pygitweb/settings_config.py +122 -0
- pygitweb/settings_config_test.py +101 -0
- pygitweb/settings_permissions_test.py +76 -0
- pygitweb/static/blob-view.js +133 -0
- pygitweb/static/board.js +839 -0
- pygitweb/static/hook-install.js +53 -0
- pygitweb/static/icons/cloud-down.svg +1 -0
- pygitweb/static/icons/cloud-exclamation.svg +1 -0
- pygitweb/static/icons/cloud-filled.svg +1 -0
- pygitweb/static/icons/cloud-storm.svg +1 -0
- pygitweb/static/icons/cloud-up.svg +1 -0
- pygitweb/static/icons/cloud.svg +1 -0
- pygitweb/static/logo.svg +44 -0
- pygitweb/static/main.css +1210 -0
- pygitweb/static/navbar-auth.js +82 -0
- pygitweb/static/readme-render.js +174 -0
- pygitweb/static/search-page.js +139 -0
- pygitweb/static/summary-ref-switcher.js +130 -0
- pygitweb/static/themes/solarized.css +34 -0
- pygitweb/tasks.py +595 -0
- pygitweb/tasks_permissions_test.py +137 -0
- pygitweb/tasks_routes_test.py +430 -0
- pygitweb/templates/addproject.html +174 -0
- pygitweb/templates/blobdiff.html +57 -0
- pygitweb/templates/board.html +31 -0
- pygitweb/templates/board_card.html +44 -0
- pygitweb/templates/commitdiff.html +57 -0
- pygitweb/templates/hooks_summary_cell.html +37 -0
- pygitweb/templates/login.html +37 -0
- pygitweb/templates/merge_request_summary_cell.html +18 -0
- pygitweb/templates/merge_request_tag.html +90 -0
- pygitweb/templates/pagination.html +44 -0
- pygitweb/templates/preamble.html +89 -0
- pygitweb/templates/project_settings.html +13 -0
- pygitweb/templates/pygit2_settings.html +12 -0
- pygitweb/templates/pygitweb_settings.html +12 -0
- pygitweb/templates/search.html +77 -0
- pygitweb/templates/settings_form_field.html +27 -0
- pygitweb/templates/table.html +15 -0
- pygitweb/templates/user_profile.html +10 -0
- pygitweb/templates_env.py +21 -0
- pygitweb/timeline_cache.py +243 -0
- pygitweb/validation.py +75 -0
- pygitweb/validation_test.py +167 -0
- pygitweb-0.1.0.dist-info/METADATA +165 -0
- pygitweb-0.1.0.dist-info/RECORD +95 -0
- pygitweb-0.1.0.dist-info/WHEEL +5 -0
- pygitweb-0.1.0.dist-info/licenses/LICENSE +21 -0
- pygitweb-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/draft-04/schema#",
|
|
3
|
+
"description": "PyGitWeb runtime settings persisted to ~/.pygitweb/settings.json (or PYGITWEB_SETTINGS_CONFIG).",
|
|
4
|
+
"type": "object",
|
|
5
|
+
"properties": {
|
|
6
|
+
"PROJECTROOT": {
|
|
7
|
+
"type": "string",
|
|
8
|
+
"minLength": 1,
|
|
9
|
+
"description": "Root filesystem path under which git repositories live."
|
|
10
|
+
},
|
|
11
|
+
"PROJECTS_LIST": {
|
|
12
|
+
"type": "string",
|
|
13
|
+
"description": "Path used for listing projects (often same as PROJECTROOT)."
|
|
14
|
+
},
|
|
15
|
+
"PROJECT_MAXDEPTH": {
|
|
16
|
+
"type": "integer",
|
|
17
|
+
"minimum": 0,
|
|
18
|
+
"description": "Maximum directory depth when scanning for projects."
|
|
19
|
+
},
|
|
20
|
+
"SITE_NAME": {
|
|
21
|
+
"type": "string",
|
|
22
|
+
"minLength": 1,
|
|
23
|
+
"description": "Site name shown in the web interface."
|
|
24
|
+
},
|
|
25
|
+
"EXPORT_OK": {
|
|
26
|
+
"type": "string",
|
|
27
|
+
"description": "If set, only repos with this file (or matching path) are listed."
|
|
28
|
+
},
|
|
29
|
+
"LIST_ALL": {
|
|
30
|
+
"type": "boolean",
|
|
31
|
+
"description": "When true, list all directories under project root without export_ok checks."
|
|
32
|
+
},
|
|
33
|
+
"STRICT_EXPORT": {
|
|
34
|
+
"type": "boolean",
|
|
35
|
+
"description": "When true, require export_ok (or path match) to list projects."
|
|
36
|
+
},
|
|
37
|
+
"GIT": {
|
|
38
|
+
"type": "string",
|
|
39
|
+
"minLength": 1,
|
|
40
|
+
"description": "Path to the git executable."
|
|
41
|
+
},
|
|
42
|
+
"MAXLOAD": {
|
|
43
|
+
"type": ["number", "null"],
|
|
44
|
+
"description": "Max load average; 503 when exceeded. null to disable."
|
|
45
|
+
},
|
|
46
|
+
"AUTH": {
|
|
47
|
+
"type": ["boolean", "null"],
|
|
48
|
+
"description": "When true, authentication is enabled. null means default-on with a warning on first run."
|
|
49
|
+
},
|
|
50
|
+
"AUTH_CONFIG": {
|
|
51
|
+
"type": "string",
|
|
52
|
+
"description": "Path to the auth JSON file. Empty uses ~/.pygitweb/auth.json."
|
|
53
|
+
},
|
|
54
|
+
"SETTINGS_CONFIG": {
|
|
55
|
+
"type": "string",
|
|
56
|
+
"description": "Path to this settings JSON file. Empty uses ~/.pygitweb/settings.json."
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
"required": [
|
|
60
|
+
"PROJECTROOT",
|
|
61
|
+
"PROJECTS_LIST",
|
|
62
|
+
"PROJECT_MAXDEPTH",
|
|
63
|
+
"SITE_NAME",
|
|
64
|
+
"EXPORT_OK",
|
|
65
|
+
"LIST_ALL",
|
|
66
|
+
"STRICT_EXPORT",
|
|
67
|
+
"GIT",
|
|
68
|
+
"MAXLOAD",
|
|
69
|
+
"AUTH",
|
|
70
|
+
"AUTH_CONFIG",
|
|
71
|
+
"SETTINGS_CONFIG"
|
|
72
|
+
]
|
|
73
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"""
|
|
2
|
+
PyGitWeb settings file: ~/.pygitweb/settings.json (or PYGITWEB_SETTINGS_CONFIG).
|
|
3
|
+
|
|
4
|
+
On first run, loads from environment and writes the file. When the file exists,
|
|
5
|
+
loads from JSON then applies explicit environment/.env overrides (with a warning).
|
|
6
|
+
Changes via the settings UI or setattr persist back to the file.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
import sys
|
|
13
|
+
from contextlib import contextmanager
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import TYPE_CHECKING
|
|
16
|
+
|
|
17
|
+
if TYPE_CHECKING:
|
|
18
|
+
from pygitweb.config import Settings
|
|
19
|
+
|
|
20
|
+
DEFAULT_SETTINGS_CONFIG_PATH = Path.home() / ".pygitweb" / "settings.json"
|
|
21
|
+
|
|
22
|
+
_persist_enabled = False
|
|
23
|
+
_batch_update = False
|
|
24
|
+
_active_path: Path | None = None
|
|
25
|
+
_setattr_patched = False
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def settings_config_path() -> Path:
|
|
29
|
+
explicit = __import__("os").environ.get("PYGITWEB_SETTINGS_CONFIG", "").strip()
|
|
30
|
+
if explicit:
|
|
31
|
+
return Path(explicit).expanduser()
|
|
32
|
+
return DEFAULT_SETTINGS_CONFIG_PATH
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def write_settings_file(settings: Settings, path: Path | None = None) -> None:
|
|
36
|
+
target = path or _active_path or settings_config_path()
|
|
37
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
38
|
+
target.write_text(
|
|
39
|
+
json.dumps(settings.model_dump(mode="json"), indent=2) + "\n",
|
|
40
|
+
encoding="utf-8",
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _warn_env_overrides(path: Path, fields: list[str]) -> None:
|
|
45
|
+
names = ", ".join(fields)
|
|
46
|
+
print(
|
|
47
|
+
f"WARNING: PyGitWeb: environment overrides settings file {path}\n Overridden field(s): {names}\n",
|
|
48
|
+
file=sys.stderr,
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _apply_env_overrides(base: Settings) -> list[str]:
|
|
53
|
+
from pygitweb.config import Settings
|
|
54
|
+
|
|
55
|
+
env_settings = Settings()
|
|
56
|
+
overrides: list[str] = []
|
|
57
|
+
for name in env_settings.model_fields_set:
|
|
58
|
+
env_val = getattr(env_settings, name)
|
|
59
|
+
if getattr(base, name) == env_val:
|
|
60
|
+
continue
|
|
61
|
+
setattr(base, name, env_val)
|
|
62
|
+
overrides.append(name)
|
|
63
|
+
return overrides
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _patch_settings_setattr() -> None:
|
|
67
|
+
global _setattr_patched
|
|
68
|
+
if _setattr_patched:
|
|
69
|
+
return
|
|
70
|
+
|
|
71
|
+
from pygitweb.config import Settings
|
|
72
|
+
|
|
73
|
+
original = Settings.__setattr__
|
|
74
|
+
|
|
75
|
+
def _setattr(self: Settings, name: str, value: object) -> None:
|
|
76
|
+
original(self, name, value)
|
|
77
|
+
if not _persist_enabled or _batch_update:
|
|
78
|
+
return
|
|
79
|
+
if name not in Settings.model_fields:
|
|
80
|
+
return
|
|
81
|
+
write_settings_file(self)
|
|
82
|
+
|
|
83
|
+
Settings.__setattr__ = _setattr # type: ignore[method-assign]
|
|
84
|
+
_setattr_patched = True
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def init_settings() -> Settings:
|
|
88
|
+
from pygitweb.config import Settings, _finalize_auth_settings
|
|
89
|
+
|
|
90
|
+
global _persist_enabled, _active_path
|
|
91
|
+
|
|
92
|
+
_persist_enabled = False
|
|
93
|
+
path = settings_config_path()
|
|
94
|
+
_active_path = path
|
|
95
|
+
|
|
96
|
+
if path.is_file():
|
|
97
|
+
settings = Settings.model_validate_json(path.read_text(encoding="utf-8"))
|
|
98
|
+
overrides = _apply_env_overrides(settings)
|
|
99
|
+
if overrides:
|
|
100
|
+
_warn_env_overrides(path, overrides)
|
|
101
|
+
else:
|
|
102
|
+
settings = Settings()
|
|
103
|
+
|
|
104
|
+
_finalize_auth_settings(settings)
|
|
105
|
+
write_settings_file(settings, path)
|
|
106
|
+
_patch_settings_setattr()
|
|
107
|
+
_persist_enabled = True
|
|
108
|
+
return settings
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
@contextmanager
|
|
112
|
+
def settings_batch_update(settings: Settings):
|
|
113
|
+
"""Apply multiple setting changes and persist once."""
|
|
114
|
+
global _batch_update
|
|
115
|
+
|
|
116
|
+
_batch_update = True
|
|
117
|
+
try:
|
|
118
|
+
yield
|
|
119
|
+
finally:
|
|
120
|
+
_batch_update = False
|
|
121
|
+
if _persist_enabled:
|
|
122
|
+
write_settings_file(settings)
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
import pytest
|
|
5
|
+
|
|
6
|
+
from pygitweb.config import Settings
|
|
7
|
+
from pygitweb.settings_config import init_settings, settings_batch_update, write_settings_file
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@pytest.fixture
|
|
11
|
+
def settings_path(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Path:
|
|
12
|
+
path = tmp_path / "settings.json"
|
|
13
|
+
monkeypatch.setenv("PYGITWEB_SETTINGS_CONFIG", str(path))
|
|
14
|
+
monkeypatch.setenv("PYGITWEB_AUTH", "0")
|
|
15
|
+
return path
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def test_creates_settings_file_from_environment(settings_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
19
|
+
monkeypatch.setenv("PYGITWEB_SITE_NAME", "FromEnv")
|
|
20
|
+
settings = init_settings()
|
|
21
|
+
assert settings_path.is_file()
|
|
22
|
+
data = json.loads(settings_path.read_text(encoding="utf-8"))
|
|
23
|
+
assert data["SITE_NAME"] == "FromEnv"
|
|
24
|
+
assert settings.SITE_NAME == "FromEnv"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def test_loads_existing_settings_file(settings_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
28
|
+
settings_path.write_text(
|
|
29
|
+
json.dumps({
|
|
30
|
+
"PROJECTROOT": "/var/git",
|
|
31
|
+
"PROJECTS_LIST": "/var/git",
|
|
32
|
+
"PROJECT_MAXDEPTH": 2,
|
|
33
|
+
"SITE_NAME": "FromFile",
|
|
34
|
+
"EXPORT_OK": "",
|
|
35
|
+
"LIST_ALL": True,
|
|
36
|
+
"STRICT_EXPORT": False,
|
|
37
|
+
"GIT": "git",
|
|
38
|
+
"MAXLOAD": None,
|
|
39
|
+
"AUTH": False,
|
|
40
|
+
"AUTH_CONFIG": "",
|
|
41
|
+
"SETTINGS_CONFIG": "",
|
|
42
|
+
})
|
|
43
|
+
+ "\n",
|
|
44
|
+
encoding="utf-8",
|
|
45
|
+
)
|
|
46
|
+
monkeypatch.delenv("PYGITWEB_SITE_NAME", raising=False)
|
|
47
|
+
settings = init_settings()
|
|
48
|
+
assert settings.SITE_NAME == "FromFile"
|
|
49
|
+
assert settings.PROJECT_MAXDEPTH == 2
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def test_env_overrides_settings_file(settings_path: Path, monkeypatch: pytest.MonkeyPatch, capsys) -> None:
|
|
53
|
+
settings_path.write_text(
|
|
54
|
+
json.dumps({
|
|
55
|
+
"PROJECTROOT": "/var/git",
|
|
56
|
+
"PROJECTS_LIST": "/var/git",
|
|
57
|
+
"PROJECT_MAXDEPTH": 1,
|
|
58
|
+
"SITE_NAME": "FromFile",
|
|
59
|
+
"EXPORT_OK": "",
|
|
60
|
+
"LIST_ALL": True,
|
|
61
|
+
"STRICT_EXPORT": False,
|
|
62
|
+
"GIT": "git",
|
|
63
|
+
"MAXLOAD": None,
|
|
64
|
+
"AUTH": False,
|
|
65
|
+
"AUTH_CONFIG": "",
|
|
66
|
+
"SETTINGS_CONFIG": "",
|
|
67
|
+
})
|
|
68
|
+
+ "\n",
|
|
69
|
+
encoding="utf-8",
|
|
70
|
+
)
|
|
71
|
+
monkeypatch.setenv("PYGITWEB_SITE_NAME", "FromEnv")
|
|
72
|
+
settings = init_settings()
|
|
73
|
+
assert settings.SITE_NAME == "FromEnv"
|
|
74
|
+
assert json.loads(settings_path.read_text(encoding="utf-8"))["SITE_NAME"] == "FromEnv"
|
|
75
|
+
err = capsys.readouterr().err
|
|
76
|
+
assert "environment overrides settings file" in err
|
|
77
|
+
assert "SITE_NAME" in err
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def test_setattr_persists_to_file(settings_path: Path) -> None:
|
|
81
|
+
settings = init_settings()
|
|
82
|
+
settings.SITE_NAME = "Updated"
|
|
83
|
+
assert json.loads(settings_path.read_text(encoding="utf-8"))["SITE_NAME"] == "Updated"
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def test_batch_update_persists_once(settings_path: Path) -> None:
|
|
87
|
+
settings = init_settings()
|
|
88
|
+
with settings_batch_update(settings):
|
|
89
|
+
settings.SITE_NAME = "BatchOne"
|
|
90
|
+
settings.GIT = "/usr/bin/git"
|
|
91
|
+
data = json.loads(settings_path.read_text(encoding="utf-8"))
|
|
92
|
+
assert data["SITE_NAME"] == "BatchOne"
|
|
93
|
+
assert data["GIT"] == "/usr/bin/git"
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def test_write_settings_file_roundtrip(settings_path: Path) -> None:
|
|
97
|
+
settings = Settings(AUTH=False, SITE_NAME="Roundtrip")
|
|
98
|
+
write_settings_file(settings, settings_path)
|
|
99
|
+
loaded = Settings.model_validate_json(settings_path.read_text(encoding="utf-8"))
|
|
100
|
+
assert loaded.SITE_NAME == "Roundtrip"
|
|
101
|
+
assert loaded.AUTH is False
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
from collections.abc import Generator
|
|
2
|
+
from unittest.mock import patch
|
|
3
|
+
|
|
4
|
+
import pytest
|
|
5
|
+
from fastapi.testclient import TestClient
|
|
6
|
+
|
|
7
|
+
from pygitweb.auth_config import AuthConfig, LocalUser
|
|
8
|
+
from pygitweb.main import app
|
|
9
|
+
from pygitweb.permissions import PERMISSION_SETTINGS, PermissionsMap
|
|
10
|
+
|
|
11
|
+
_SETTINGS_AUTH = AuthConfig(
|
|
12
|
+
auth_mode="local",
|
|
13
|
+
local_users=[LocalUser(user="admin", password="secret"), LocalUser(user="viewer", password="secret")],
|
|
14
|
+
oauth_permissions=PermissionsMap.model_validate({
|
|
15
|
+
"*": [],
|
|
16
|
+
PERMISSION_SETTINGS: ["admin"],
|
|
17
|
+
}),
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@pytest.fixture
|
|
22
|
+
def client() -> Generator[TestClient, None, None]:
|
|
23
|
+
with TestClient(app) as c:
|
|
24
|
+
yield c
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@pytest.fixture
|
|
28
|
+
def settings_auth() -> Generator[AuthConfig, None, None]:
|
|
29
|
+
with (
|
|
30
|
+
patch("pygitweb.auth_config.auth_config", _SETTINGS_AUTH),
|
|
31
|
+
patch("pygitweb.auth.auth_config", _SETTINGS_AUTH),
|
|
32
|
+
patch("pygitweb.main.auth_config", _SETTINGS_AUTH),
|
|
33
|
+
):
|
|
34
|
+
yield _SETTINGS_AUTH
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _login(client: TestClient, username: str) -> None:
|
|
38
|
+
client.post(
|
|
39
|
+
"/login",
|
|
40
|
+
data={"username": username, "password": "secret", "next": "/"},
|
|
41
|
+
follow_redirects=False,
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def test_settings_open_when_auth_disabled(client: TestClient) -> None:
|
|
46
|
+
from pygitweb.config import settings
|
|
47
|
+
|
|
48
|
+
with patch.object(settings, "AUTH", False):
|
|
49
|
+
r = client.get("/settings/pygitweb")
|
|
50
|
+
assert r.status_code == 200
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def test_settings_forbidden_without_login(client: TestClient, settings_auth: AuthConfig) -> None:
|
|
54
|
+
from pygitweb.config import settings
|
|
55
|
+
|
|
56
|
+
with patch.object(settings, "AUTH", True):
|
|
57
|
+
r = client.get("/settings/pygitweb")
|
|
58
|
+
assert r.status_code == 401
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def test_settings_allowed_with_grant(client: TestClient, settings_auth: AuthConfig) -> None:
|
|
62
|
+
from pygitweb.config import settings
|
|
63
|
+
|
|
64
|
+
with patch.object(settings, "AUTH", True):
|
|
65
|
+
_login(client, "admin")
|
|
66
|
+
r = client.get("/settings/pygitweb")
|
|
67
|
+
assert r.status_code == 200
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def test_settings_forbidden_without_grant(client: TestClient, settings_auth: AuthConfig) -> None:
|
|
71
|
+
from pygitweb.config import settings
|
|
72
|
+
|
|
73
|
+
with patch.object(settings, "AUTH", True):
|
|
74
|
+
_login(client, "viewer")
|
|
75
|
+
r = client.get("/settings/pygit2")
|
|
76
|
+
assert r.status_code == 403
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
(function(){
|
|
2
|
+
var el = document.getElementById('Highlight-script');
|
|
3
|
+
function run() {
|
|
4
|
+
var code = document.querySelector('.blob-content code');
|
|
5
|
+
if (code && typeof hljs !== 'undefined') hljs.highlightElement(code);
|
|
6
|
+
applyBlobLineSelection();
|
|
7
|
+
}
|
|
8
|
+
function parseLinesParam() {
|
|
9
|
+
var params = new URLSearchParams(window.location.search);
|
|
10
|
+
var s = params.get('lines');
|
|
11
|
+
if (!s) return [];
|
|
12
|
+
var re = /L(\d+)(?:C(\d+))?(?:-L(\d+)(?:C(\d+))?)?/g;
|
|
13
|
+
var ranges = [];
|
|
14
|
+
var m;
|
|
15
|
+
while ((m = re.exec(s)) !== null) {
|
|
16
|
+
var startLine = parseInt(m[1], 10);
|
|
17
|
+
var startCol = m[2] ? parseInt(m[2], 10) : null;
|
|
18
|
+
var endLine = m[3] !== undefined ? parseInt(m[3], 10) : null;
|
|
19
|
+
var endCol = m[4] ? parseInt(m[4], 10) : null;
|
|
20
|
+
ranges.push({ startLine: startLine, startCol: startCol, endLine: endLine, endCol: endCol });
|
|
21
|
+
}
|
|
22
|
+
return ranges;
|
|
23
|
+
}
|
|
24
|
+
function wrapCodeInLineSpans(code) {
|
|
25
|
+
var lines = [];
|
|
26
|
+
var current = [];
|
|
27
|
+
function flushLine() {
|
|
28
|
+
if (current.length) {
|
|
29
|
+
lines.push(current);
|
|
30
|
+
current = [];
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
var nodes = Array.from(code.childNodes);
|
|
34
|
+
for (var i = 0; i < nodes.length; i++) {
|
|
35
|
+
var node = nodes[i];
|
|
36
|
+
if (node.nodeType === Node.TEXT_NODE) {
|
|
37
|
+
var parts = node.textContent.split('\n');
|
|
38
|
+
for (var j = 0; j < parts.length; j++) {
|
|
39
|
+
if (j > 0) flushLine();
|
|
40
|
+
current.push(parts[j]);
|
|
41
|
+
}
|
|
42
|
+
} else {
|
|
43
|
+
current.push(node);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
flushLine();
|
|
47
|
+
code.replaceChildren();
|
|
48
|
+
for (var i = 0; i < lines.length; i++) {
|
|
49
|
+
var span = document.createElement('span');
|
|
50
|
+
span.setAttribute('data-line', String(i + 1));
|
|
51
|
+
for (var k = 0; k < lines[i].length; k++) {
|
|
52
|
+
var item = lines[i][k];
|
|
53
|
+
if (typeof item === 'string') {
|
|
54
|
+
span.appendChild(document.createTextNode(item));
|
|
55
|
+
} else {
|
|
56
|
+
span.appendChild(item);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
code.appendChild(span);
|
|
60
|
+
if (i < lines.length - 1) code.appendChild(document.createTextNode('\n'));
|
|
61
|
+
}
|
|
62
|
+
return code.querySelectorAll('[data-line]');
|
|
63
|
+
}
|
|
64
|
+
function wrapCharacterRangeInSpan(lineSpan, startChar, endChar) {
|
|
65
|
+
startChar = Math.max(0, startChar);
|
|
66
|
+
var len = lineSpan.textContent.length;
|
|
67
|
+
endChar = endChar == null ? len : Math.min(len, endChar);
|
|
68
|
+
if (startChar >= endChar) return;
|
|
69
|
+
var walker = document.createTreeWalker(lineSpan, NodeFilter.SHOW_TEXT);
|
|
70
|
+
var charIdx = 0;
|
|
71
|
+
var startNode = null, startOff = 0, endNode = null, endOff = 0;
|
|
72
|
+
var n;
|
|
73
|
+
while ((n = walker.nextNode())) {
|
|
74
|
+
var nLen = n.length;
|
|
75
|
+
if (startNode == null && charIdx + nLen > startChar) {
|
|
76
|
+
startNode = n;
|
|
77
|
+
startOff = startChar - charIdx;
|
|
78
|
+
}
|
|
79
|
+
if (endNode == null && charIdx + nLen >= endChar) {
|
|
80
|
+
endNode = n;
|
|
81
|
+
endOff = endChar - charIdx;
|
|
82
|
+
break;
|
|
83
|
+
}
|
|
84
|
+
charIdx += nLen;
|
|
85
|
+
}
|
|
86
|
+
if (!startNode || !endNode) return;
|
|
87
|
+
try {
|
|
88
|
+
var range = document.createRange();
|
|
89
|
+
range.setStart(startNode, startOff);
|
|
90
|
+
range.setEnd(endNode, endOff);
|
|
91
|
+
var span = document.createElement('span');
|
|
92
|
+
span.className = 'blob-selection';
|
|
93
|
+
range.surroundContents(span);
|
|
94
|
+
} catch (e) {}
|
|
95
|
+
}
|
|
96
|
+
function applyBlobLineSelection() {
|
|
97
|
+
var code = document.querySelector('.blob-content code');
|
|
98
|
+
if (!code) return;
|
|
99
|
+
var ranges = parseLinesParam();
|
|
100
|
+
if (!ranges.length) return;
|
|
101
|
+
var lineSpans = wrapCodeInLineSpans(code);
|
|
102
|
+
var firstTarget = null;
|
|
103
|
+
for (var r = 0; r < ranges.length; r++) {
|
|
104
|
+
var range = ranges[r];
|
|
105
|
+
var startLine = range.startLine;
|
|
106
|
+
var startCol = range.startCol;
|
|
107
|
+
var endLine = range.endLine != null ? range.endLine : startLine;
|
|
108
|
+
var endCol = range.endCol;
|
|
109
|
+
for (var i = startLine; i <= endLine; i++) {
|
|
110
|
+
var span = code.querySelector('[data-line="' + i + '"]');
|
|
111
|
+
if (!span) continue;
|
|
112
|
+
if (firstTarget === null) firstTarget = span;
|
|
113
|
+
var lineLen = span.textContent.length;
|
|
114
|
+
var startCh = (i === startLine && startCol != null) ? startCol - 1 : 0;
|
|
115
|
+
var endCh = (i === endLine && endCol != null) ? endCol : lineLen;
|
|
116
|
+
if (startCh === 0 && endCh >= lineLen) {
|
|
117
|
+
span.classList.add('blob-line-selected');
|
|
118
|
+
} else {
|
|
119
|
+
wrapCharacterRangeInSpan(span, startCh, endCh);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
if (firstTarget) {
|
|
124
|
+
firstTarget.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
if (el) {
|
|
128
|
+
if (el.getAttribute('data-loaded')) run();
|
|
129
|
+
else el.addEventListener('load', function(){ el.setAttribute('data-loaded','1'); run(); });
|
|
130
|
+
} else {
|
|
131
|
+
run();
|
|
132
|
+
}
|
|
133
|
+
})();
|