1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
"""
Project list: get_projects_list, search_projects_list, project_in_list,
get_project_owner, get_project_list_from_file, get_last_activity.
Ported from gitweb/gitweb.perl.
"""
from __future__ import annotations
import os
import re
from collections.abc import Callable
from typing import Any
import pygit2
from pygitweb.config import LIST_ALL, PROJECT_MAXDEPTH, PROJECTROOT, PROJECTS_LIST
from pygitweb.validation import check_export_ok
def _export_ok_path(git_dir: str, export_ok: str) -> bool:
return not export_ok or os.path.isfile(os.path.join(git_dir, export_ok))
def project_in_list(
project: str,
get_projects_list_fn: Callable[[], list[dict[str, Any]]],
) -> bool:
"""True if project appears in project list. Port of project_in_list."""
lst = get_projects_list_fn()
return any(p.get("path") == project for p in lst)
def _find_projects_in_dir(
root: str,
prefix_len: int,
prefix_depth: int,
maxdepth: int,
export_ok: str,
export_auth_hook: Callable[[str], bool] | None,
skip_export_check: bool = False,
) -> list[dict[str, Any]]:
result = []
for dirpath, dirnames, _ in os.walk(root, topdown=True):
rel = os.path.relpath(dirpath, root)
if rel == ".":
depth = 0
else:
depth = rel.count(os.sep) + 1
if depth >= maxdepth:
dirnames.clear()
continue
for d in list(dirnames):
path = os.path.join(dirpath, d)
if not os.path.isdir(path):
continue
try:
if not os.access(path, os.X_OK):
continue
except OSError:
continue
repo = pygit2.discover_repository(path)
if not repo:
continue
project_path = os.path.relpath(path, PROJECTROOT)
project_path = project_path.replace("\\", "/")
git_dir = os.path.join(PROJECTROOT, project_path)
if not skip_export_check and not check_export_ok(git_dir, export_ok, export_auth_hook):
continue
result.append({"path": project_path})
dirnames.remove(d)
return result
def git_get_projects_list(
filter_path: str = "",
paranoid: bool = False,
projectroot: str = PROJECTROOT,
projects_list: str = PROJECTS_LIST,
project_maxdepth: int = PROJECT_MAXDEPTH,
export_ok: str = "",
export_auth_hook: Callable[[str], bool] | None = None,
) -> list[dict[str, Any]]:
"""List projects from directory scan or file. Port of git_get_projects_list."""
if os.path.isdir(projects_list):
root = projects_list.rstrip("/")
prefix_len = len(root) + 1
prefix_depth = root.count(os.sep)
if filter_path and not paranoid:
root = os.path.join(root, filter_path).rstrip("/")
result = _find_projects_in_dir(
root,
prefix_len,
prefix_depth,
project_maxdepth,
export_ok,
export_auth_hook,
skip_export_check=LIST_ALL,
)
if filter_path and paranoid:
result = [p for p in result if p["path"].startswith(filter_path + "/")]
return result
if os.path.isfile(projects_list):
from urllib.parse import unquote
result = []
with open(projects_list) as f:
for line in f:
line = line.strip()
if not line:
continue
parts = line.split(None, 1)
path = unquote(parts[0]) if parts else ""
owner = unquote(parts[1]) if len(parts) > 1 else None
if not path:
continue
if filter_path and not path.startswith(filter_path + "/"):
continue
git_dir = os.path.join(projectroot, path)
if not LIST_ALL and not check_export_ok(git_dir, export_ok, export_auth_hook):
continue
pr = {"path": path}
if owner:
pr["owner"] = owner
result.append(pr)
return result
return []
_gitweb_project_owner: dict[str, str] | None = None
def git_get_project_list_from_file(
projects_list: str = PROJECTS_LIST,
projectroot: str = PROJECTROOT,
) -> dict[str, str]:
"""Load project -> owner from file. Port of git_get_project_list_from_file."""
global _gitweb_project_owner
if _gitweb_project_owner is not None:
return _gitweb_project_owner
_gitweb_project_owner = {}
if os.path.isfile(projects_list):
from urllib.parse import unquote
with open(projects_list) as f:
for line in f:
line = line.strip()
if not line:
continue
parts = line.split(None, 1)
path = unquote(parts[0]) if parts else ""
owner = unquote(parts[1]) if len(parts) > 1 else ""
if path:
_gitweb_project_owner[path] = owner
return _gitweb_project_owner
def git_get_project_owner(
project: str,
projectroot: str = PROJECTROOT,
get_project_config: Callable[[str, str], Any] | None = None,
) -> str | None:
"""Owner from list file or config or file ownership. Port of git_get_project_owner."""
if not project:
return None
owners = git_get_project_list_from_file(PROJECTS_LIST, projectroot)
if project in owners and owners[project]:
return owners[project]
if get_project_config:
val = get_project_config(project, "owner")
if val:
return val[0] if isinstance(val, list) else val
return None
def git_get_last_activity(project: str, projectroot: str = PROJECTROOT) -> int | None:
"""Last commit timestamp for project. Port of git_get_last_activity."""
from pygitweb.git_helpers import git_get_head_hash, parse_commit
oid = git_get_head_hash(project)
if not oid:
return None
co = parse_commit(project, oid)
return co.get("committer_epoch")
def search_projects_list(
projlist: list[dict[str, Any]],
tagfilter: str | None = None,
search_regexp: str | None = None,
fill_project_list_info: Callable[..., None] | None = None,
) -> list[dict[str, Any]]:
"""Filter by tag or search regex. Port of search_projects_list."""
if not tagfilter and not search_regexp:
return projlist
if fill_project_list_info:
fill_project_list_info(projlist, tagfilter=tagfilter, search_re=search_regexp)
result = []
for pr in projlist:
if tagfilter:
ctags = pr.get("ctags") or {}
if not any(k.lower() == tagfilter.lower() for k in ctags):
continue
if search_regexp:
try:
rex = re.compile(search_regexp)
except re.error:
continue
descr = (pr.get("descr_long") or "") + (pr.get("path") or "")
if not rex.search(descr):
continue
result.append(pr)
return result