@kernel-sig/console 0.1.0
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.
- package/.env.example +50 -0
- package/LICENSE +127 -0
- package/Makefile +66 -0
- package/README.md +279 -0
- package/backend/.dockerignore +12 -0
- package/backend/Dockerfile +43 -0
- package/backend/alembic/env.py +75 -0
- package/backend/alembic/script.py.mako +25 -0
- package/backend/alembic/versions/0001_initial.py +88 -0
- package/backend/alembic/versions/350e2d9b6553_add_pr_comment_table.py +48 -0
- package/backend/alembic/versions/4f8342e727fe_sig_info_roster_fields_and_branch_.py +39 -0
- package/backend/alembic/versions/5a31ade90136_add_repository_credential_pull_request_.py +293 -0
- package/backend/alembic/versions/87a008142f17_add_classification_attention_and_ai_.py +436 -0
- package/backend/alembic/versions/a1c7d90e4b52_branch_belongs_to_a_repository.py +72 -0
- package/backend/alembic/versions/b41c9d7e5f28_extend_pr_kind_categories.py +43 -0
- package/backend/alembic/versions/c8e4f2a71b93_add_release_kind.py +40 -0
- package/backend/alembic/versions/d1275091dfdc_add_needs_detail_flag_to_pull_request.py +44 -0
- package/backend/alembic/versions/e5b27c9d3a41_downgrade_cve_without_ids.py +54 -0
- package/backend/alembic/versions/ee3159a4eff0_add_sig_meeting_member_and_release_.py +164 -0
- package/backend/alembic/versions/fcf3c186d63b_attention_rule_subscriptions.py +42 -0
- package/backend/alembic.ini +40 -0
- package/backend/app/__init__.py +0 -0
- package/backend/app/api/__init__.py +0 -0
- package/backend/app/api/deps.py +74 -0
- package/backend/app/api/v1/__init__.py +0 -0
- package/backend/app/api/v1/ai.py +386 -0
- package/backend/app/api/v1/analytics.py +48 -0
- package/backend/app/api/v1/attention.py +142 -0
- package/backend/app/api/v1/auth.py +106 -0
- package/backend/app/api/v1/classification.py +192 -0
- package/backend/app/api/v1/health.py +29 -0
- package/backend/app/api/v1/issues.py +143 -0
- package/backend/app/api/v1/pulls.py +388 -0
- package/backend/app/api/v1/repositories.py +273 -0
- package/backend/app/api/v1/router.py +32 -0
- package/backend/app/api/v1/sig.py +565 -0
- package/backend/app/api/v1/users.py +87 -0
- package/backend/app/api/v1/webhooks.py +135 -0
- package/backend/app/core/__init__.py +0 -0
- package/backend/app/core/config.py +72 -0
- package/backend/app/core/crypto.py +74 -0
- package/backend/app/core/db.py +79 -0
- package/backend/app/core/exceptions.py +60 -0
- package/backend/app/core/logging.py +69 -0
- package/backend/app/core/permissions.py +87 -0
- package/backend/app/core/queue.py +57 -0
- package/backend/app/core/security.py +69 -0
- package/backend/app/domain/__init__.py +0 -0
- package/backend/app/domain/attention.py +618 -0
- package/backend/app/domain/classification.py +689 -0
- package/backend/app/domain/meeting.py +408 -0
- package/backend/app/domain/release.py +163 -0
- package/backend/app/domain/review.py +416 -0
- package/backend/app/domain/sig.py +178 -0
- package/backend/app/domain/sig_info.py +201 -0
- package/backend/app/integrations/__init__.py +0 -0
- package/backend/app/integrations/atomgit/__init__.py +0 -0
- package/backend/app/integrations/atomgit/client.py +505 -0
- package/backend/app/integrations/atomgit/models.py +311 -0
- package/backend/app/integrations/llm/__init__.py +0 -0
- package/backend/app/integrations/llm/prompts.py +222 -0
- package/backend/app/integrations/llm/provider.py +326 -0
- package/backend/app/main.py +242 -0
- package/backend/app/middleware/__init__.py +0 -0
- package/backend/app/middleware/audit.py +129 -0
- package/backend/app/middleware/request_context.py +42 -0
- package/backend/app/models/__init__.py +112 -0
- package/backend/app/models/ai.py +214 -0
- package/backend/app/models/attention.py +131 -0
- package/backend/app/models/attention_settings.py +61 -0
- package/backend/app/models/audit.py +53 -0
- package/backend/app/models/base.py +35 -0
- package/backend/app/models/classification.py +144 -0
- package/backend/app/models/credential.py +56 -0
- package/backend/app/models/issue.py +132 -0
- package/backend/app/models/meeting.py +189 -0
- package/backend/app/models/pull_request.py +288 -0
- package/backend/app/models/repository.py +196 -0
- package/backend/app/models/sig.py +194 -0
- package/backend/app/models/user.py +44 -0
- package/backend/app/schemas/__init__.py +0 -0
- package/backend/app/schemas/ai.py +120 -0
- package/backend/app/schemas/attention.py +43 -0
- package/backend/app/schemas/auth.py +26 -0
- package/backend/app/schemas/classification.py +58 -0
- package/backend/app/schemas/common.py +43 -0
- package/backend/app/schemas/pull_request.py +214 -0
- package/backend/app/schemas/repository.py +94 -0
- package/backend/app/schemas/sig.py +246 -0
- package/backend/app/schemas/user.py +79 -0
- package/backend/app/services/__init__.py +0 -0
- package/backend/app/services/ai_service.py +569 -0
- package/backend/app/services/analytics_service.py +390 -0
- package/backend/app/services/attention_queue.py +451 -0
- package/backend/app/services/attention_service.py +432 -0
- package/backend/app/services/auth_service.py +74 -0
- package/backend/app/services/classification_service.py +658 -0
- package/backend/app/services/credential_service.py +105 -0
- package/backend/app/services/pull_query.py +273 -0
- package/backend/app/services/release_service.py +446 -0
- package/backend/app/services/repository_service.py +132 -0
- package/backend/app/services/sig_service.py +385 -0
- package/backend/app/services/sync_service.py +752 -0
- package/backend/app/services/user_service.py +68 -0
- package/backend/app/worker.py +389 -0
- package/backend/entrypoint.sh +10 -0
- package/backend/pyproject.toml +68 -0
- package/backend/tests/test_analysis_api.py +154 -0
- package/backend/tests/test_atomgit_client.py +360 -0
- package/backend/tests/test_atomgit_models.py +257 -0
- package/backend/tests/test_attention.py +291 -0
- package/backend/tests/test_audit_middleware.py +135 -0
- package/backend/tests/test_classification.py +498 -0
- package/backend/tests/test_config.py +41 -0
- package/backend/tests/test_crypto.py +68 -0
- package/backend/tests/test_exceptions.py +62 -0
- package/backend/tests/test_health.py +63 -0
- package/backend/tests/test_llm_provider.py +320 -0
- package/backend/tests/test_meeting_domain.py +169 -0
- package/backend/tests/test_permissions.py +69 -0
- package/backend/tests/test_pull_query_wiring.py +66 -0
- package/backend/tests/test_pull_schemas.py +82 -0
- package/backend/tests/test_release_domain.py +82 -0
- package/backend/tests/test_review_parser.py +301 -0
- package/backend/tests/test_schemas_user.py +97 -0
- package/backend/tests/test_security.py +92 -0
- package/cli/index.js +338 -0
- package/compose.yaml +105 -0
- package/frontend/.dockerignore +4 -0
- package/frontend/Dockerfile +27 -0
- package/frontend/index.html +14 -0
- package/frontend/nginx.conf +47 -0
- package/frontend/package-lock.json +5020 -0
- package/frontend/package.json +35 -0
- package/frontend/src/api/ai.ts +124 -0
- package/frontend/src/api/analytics.ts +66 -0
- package/frontend/src/api/attention.ts +181 -0
- package/frontend/src/api/auth.ts +74 -0
- package/frontend/src/api/classification.ts +170 -0
- package/frontend/src/api/issues.ts +90 -0
- package/frontend/src/api/pulls.ts +233 -0
- package/frontend/src/api/repositories.ts +95 -0
- package/frontend/src/api/sig.ts +232 -0
- package/frontend/src/app/antd-theme.ts +94 -0
- package/frontend/src/app/providers.tsx +59 -0
- package/frontend/src/app/router.tsx +411 -0
- package/frontend/src/app/search.ts +30 -0
- package/frontend/src/components/ClassificationBadge.tsx +58 -0
- package/frontend/src/components/GateBadge.tsx +13 -0
- package/frontend/src/components/SeverityBadge.tsx +20 -0
- package/frontend/src/components/layout/AppShell.tsx +16 -0
- package/frontend/src/components/layout/AuthLayout.tsx +32 -0
- package/frontend/src/components/layout/Sidebar.tsx +223 -0
- package/frontend/src/components/layout/TopBar.tsx +47 -0
- package/frontend/src/components/pulls/DiscussionTimeline.tsx +145 -0
- package/frontend/src/components/pulls/FacetRail.tsx +199 -0
- package/frontend/src/components/pulls/LabelChips.tsx +87 -0
- package/frontend/src/components/ui/alert.tsx +30 -0
- package/frontend/src/components/ui/badge.tsx +53 -0
- package/frontend/src/components/ui/button.tsx +51 -0
- package/frontend/src/components/ui/card.tsx +64 -0
- package/frontend/src/components/ui/chart-theme.ts +65 -0
- package/frontend/src/components/ui/data-table.tsx +39 -0
- package/frontend/src/components/ui/echart.tsx +70 -0
- package/frontend/src/components/ui/empty-state.tsx +22 -0
- package/frontend/src/components/ui/input.tsx +39 -0
- package/frontend/src/components/ui/lazy-chart.tsx +21 -0
- package/frontend/src/components/ui/skeleton.tsx +19 -0
- package/frontend/src/hooks/use-current-repository.ts +44 -0
- package/frontend/src/hooks/use-current-user.ts +38 -0
- package/frontend/src/lib/api-client.ts +93 -0
- package/frontend/src/lib/css-color.ts +60 -0
- package/frontend/src/lib/utils.ts +45 -0
- package/frontend/src/main.tsx +22 -0
- package/frontend/src/pages/attention/AttentionQueuePage.tsx +316 -0
- package/frontend/src/pages/attention/RuleSettingsPanel.tsx +177 -0
- package/frontend/src/pages/branches/BranchDetailPage.tsx +632 -0
- package/frontend/src/pages/branches/BranchListPage.tsx +308 -0
- package/frontend/src/pages/dashboard/DashboardPage.tsx +657 -0
- package/frontend/src/pages/issues/IssueListPage.tsx +284 -0
- package/frontend/src/pages/login/LoginPage.tsx +95 -0
- package/frontend/src/pages/meetings/MeetingDetailPage.tsx +403 -0
- package/frontend/src/pages/meetings/MeetingListPage.tsx +264 -0
- package/frontend/src/pages/members/MembersPage.tsx +534 -0
- package/frontend/src/pages/pulls/PullDetailPage.tsx +833 -0
- package/frontend/src/pages/pulls/PullListPage.tsx +399 -0
- package/frontend/src/pages/settings/AiSettingsPage.tsx +449 -0
- package/frontend/src/pages/settings/SettingsPage.tsx +321 -0
- package/frontend/src/pages/setup/SetupPage.tsx +144 -0
- package/frontend/src/styles/globals.css +147 -0
- package/frontend/tsconfig.json +22 -0
- package/frontend/vite.config.ts +57 -0
- package/package.json +48 -0
- package/scripts/e2e-auth-flow.py +131 -0
- package/scripts/e2e-pr-detail.py +128 -0
- package/scripts/e2e-verify.py +773 -0
- package/scripts/verify-ai-pipeline.py +616 -0
- package/scripts/verify-analysis-pipeline.py +303 -0
|
@@ -0,0 +1,408 @@
|
|
|
1
|
+
"""SIG 例会纪要解析。
|
|
2
|
+
|
|
3
|
+
数据源是 Etherpad 上的活文档 ``etherpad.openeuler.org/p/Kernel-meetings``,
|
|
4
|
+
历次会议按时间倒序累积在同一页。它是人写的自由文本,所以这里的解析器
|
|
5
|
+
按"尽力而为"设计:能认出来的字段就取,认不出来就跳过,绝不抛异常。
|
|
6
|
+
|
|
7
|
+
理由很直接 —— 下一次有人换个格式写纪要时,抛异常会让整场会议的数据
|
|
8
|
+
一起消失;而少解析一个字段只是少一条信息。上游是外部输入,不能要求它
|
|
9
|
+
稳定。
|
|
10
|
+
|
|
11
|
+
纪要里最有价值的部分是「进展update」议题,它其实是**结构化的版本合入报告**:
|
|
12
|
+
|
|
13
|
+
【OLK-6.6】
|
|
14
|
+
OLK-6.6开发分支tag更新到6.6.0-170.0.0,期间合入补丁590个
|
|
15
|
+
git log 6.6.0-167.0.0..6.6.0-170.0.0 --oneline --no-merges | wc -l
|
|
16
|
+
ISO镜像和update rpm包获取链接:
|
|
17
|
+
https://repo.openeuler.org/openEuler-24.03-LTS-SP1
|
|
18
|
+
CVE: (124)
|
|
19
|
+
CVE-2026-74739,CVE-2026-74544,...
|
|
20
|
+
Bugfix:
|
|
21
|
+
nfs: use nfsi->rwsem to protect traversal of the file lock list
|
|
22
|
+
https://atomgit.com/openeuler/kernel/pull/26235
|
|
23
|
+
Feature:
|
|
24
|
+
...
|
|
25
|
+
|
|
26
|
+
这一段既是双周合入总结,也是各版本的 release notes:tag 区间、补丁数、
|
|
27
|
+
CVE 清单、按类别归好的补丁列表。解析出来就能直接把 PR 号链回本地 PR 表。
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
from __future__ import annotations
|
|
31
|
+
|
|
32
|
+
import re
|
|
33
|
+
from dataclasses import dataclass, field
|
|
34
|
+
from datetime import date
|
|
35
|
+
from itertools import pairwise
|
|
36
|
+
|
|
37
|
+
from app.domain.release import normalize_branch_name
|
|
38
|
+
|
|
39
|
+
# 【2026/9/4 14:00 Kernel SIG 双周例会】;早期会议没写时间
|
|
40
|
+
_MEETING_HEADER = re.compile(
|
|
41
|
+
r"^【\s*(\d{4})/(\d{1,2})/(\d{1,2})(?:\s+(\d{1,2}:\d{2}))?\s*([^】]+?)\s*】\s*$"
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
# 【OLK-6.6】—— 必须是整行只有标签,否则是「【OLK-6.6】xxx」这种带前缀的议题标题
|
|
45
|
+
_PROGRESS_HEADER = re.compile(r"^【\s*([^】]+?)\s*】\s*$")
|
|
46
|
+
|
|
47
|
+
# 一、上期遗留问题跟踪 / 二、议题列表
|
|
48
|
+
_SECTION = re.compile(r"^([一二三四五六七八九十]+)、\s*(.+)$")
|
|
49
|
+
|
|
50
|
+
# 议题一:进展update(吴腾达);编号有重复也有占位的 X
|
|
51
|
+
_AGENDA = re.compile(r"^议题\s*([一二三四五六七八九十\dXx]+)\s*[::]\s*(.*)$")
|
|
52
|
+
|
|
53
|
+
# 轮值主持:关文涛【轮值主持顺序:曾昭荣->桑力鹏->...】
|
|
54
|
+
_HOST = re.compile(r"^轮值主持\s*[::]\s*([^【((]+)")
|
|
55
|
+
_HOST_ORDER = re.compile(r"【[^】]*顺序\s*[::]\s*([^】]+)】")
|
|
56
|
+
_NEXT_HOST = re.compile(r"^下次轮值主持\s*[::]\s*(.+?)\s*$")
|
|
57
|
+
_MEETING_URL = re.compile(r"^会议链接\s*[::]\s*(\S+)\s*$")
|
|
58
|
+
_MINUTES_URL = re.compile(r"^会议纪要\s*[::]\s*(\S+)\s*$")
|
|
59
|
+
|
|
60
|
+
# 分支 tag 更新:OLK-6.6开发分支tag更新到6.6.0-170.0.0,期间合入补丁590个
|
|
61
|
+
_TAG_UPDATE = re.compile(r"更新到\s*([\w.\-]+)\s*[,,]\s*期间合入补丁\s*(\d+)\s*个")
|
|
62
|
+
# git log 6.6.0-167.0.0..6.6.0-170.0.0 --oneline --no-merges | wc -l
|
|
63
|
+
_GIT_RANGE = re.compile(r"git log\s+([\w.\-]+)\.\.([\w.\-]+)")
|
|
64
|
+
_ISO_SECTION = re.compile(r"^ISO\s*镜像|^ISO镜像|获取链接\s*[::]")
|
|
65
|
+
_CVE_SECTION = re.compile(r"^CVE\s*[::]\s*[((]?\s*(\d*)\s*[))]?\s*$")
|
|
66
|
+
_CVE_ID = re.compile(r"CVE-\d{4}-\d{4,7}")
|
|
67
|
+
|
|
68
|
+
# 分组标题:短的、以冒号结尾、可带 (N) 计数。Bugfix: / LTS:/ 灵衢: (13)
|
|
69
|
+
_PROGRESS_GROUP = re.compile(r"^([^::\s][^::]{0,23}?)\s*[::]\s*(?:[((]\s*(\d+)\s*[))])?\s*$")
|
|
70
|
+
|
|
71
|
+
# 缩进的 URL 行(补丁链接、镜像链接)
|
|
72
|
+
_INDENTED_URL = re.compile(r"^\s+(\S+://\S+)\s*$")
|
|
73
|
+
|
|
74
|
+
# PR / Issue 链接。atomgit 与 gitcode 都在用,都要认。
|
|
75
|
+
_LINK = re.compile(
|
|
76
|
+
r"https?://(?:www\.)?(?:atomgit\.com|gitcode\.com)/[\w.\-]+/[\w.\-]+/"
|
|
77
|
+
r"(pull|merge_requests|issues?)/(\d+)",
|
|
78
|
+
re.IGNORECASE,
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
# 被截断的链接,末尾没有编号(实测存在,如 ".../pull/")
|
|
82
|
+
_TRUNCATED_LINK = re.compile(
|
|
83
|
+
r"https?://(?:www\.)?(?:atomgit\.com|gitcode\.com)/[\w.\-]+/[\w.\-]+/(?:pull|issues?)/\s*$",
|
|
84
|
+
re.IGNORECASE,
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
# 分组标题里出现这些词说明它不是补丁分组,是别的段落标题
|
|
88
|
+
_NOT_A_GROUP = ("ISO", "获取链接", "git log", "镜像")
|
|
89
|
+
|
|
90
|
+
# 纪要里留着给人填写的模板行,不是真议题。
|
|
91
|
+
# 当成议题收录会让每场会议都凭空多出一个汇报人是"汇报人"的条目。
|
|
92
|
+
_TEMPLATE_MARKERS = ("其他议题在这里补充", "议题在这里补充", "格式:议题名称")
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
@dataclass
|
|
96
|
+
class ProgressItem:
|
|
97
|
+
"""进展报告里的一条补丁。"""
|
|
98
|
+
|
|
99
|
+
title: str
|
|
100
|
+
pull_number: int | None = None
|
|
101
|
+
url: str | None = None
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
@dataclass
|
|
105
|
+
class ProgressGroup:
|
|
106
|
+
"""补丁分组。``count`` 是作者自己写的条数,可能与实际条数不一致。"""
|
|
107
|
+
|
|
108
|
+
name: str
|
|
109
|
+
count: int | None = None
|
|
110
|
+
items: list[ProgressItem] = field(default_factory=list)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
@dataclass
|
|
114
|
+
class BranchProgress:
|
|
115
|
+
"""一个分支在一期内的合入情况。"""
|
|
116
|
+
|
|
117
|
+
branch: str
|
|
118
|
+
from_tag: str | None = None
|
|
119
|
+
to_tag: str | None = None
|
|
120
|
+
patch_count: int | None = None
|
|
121
|
+
"""作者自报的合入补丁数。"""
|
|
122
|
+
|
|
123
|
+
iso_urls: list[str] = field(default_factory=list)
|
|
124
|
+
cve_ids: list[str] = field(default_factory=list)
|
|
125
|
+
groups: list[ProgressGroup] = field(default_factory=list)
|
|
126
|
+
|
|
127
|
+
@property
|
|
128
|
+
def listed_patch_count(self) -> int:
|
|
129
|
+
"""分组里实际列出的补丁条数。与 ``patch_count`` 不必然相等。"""
|
|
130
|
+
return sum(len(group.items) for group in self.groups)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
@dataclass
|
|
134
|
+
class AgendaDraft:
|
|
135
|
+
num: str
|
|
136
|
+
title: str
|
|
137
|
+
owner: str | None
|
|
138
|
+
body: str
|
|
139
|
+
pull_numbers: list[int] = field(default_factory=list)
|
|
140
|
+
issue_numbers: list[int] = field(default_factory=list)
|
|
141
|
+
progress: list[BranchProgress] = field(default_factory=list)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
@dataclass
|
|
145
|
+
class MeetingDraft:
|
|
146
|
+
date: date
|
|
147
|
+
time: str | None
|
|
148
|
+
title: str
|
|
149
|
+
host: str | None = None
|
|
150
|
+
host_order: list[str] = field(default_factory=list)
|
|
151
|
+
next_host: str | None = None
|
|
152
|
+
meeting_url: str | None = None
|
|
153
|
+
minutes_url: str | None = None
|
|
154
|
+
legacy: str = ""
|
|
155
|
+
agendas: list[AgendaDraft] = field(default_factory=list)
|
|
156
|
+
|
|
157
|
+
@property
|
|
158
|
+
def pull_numbers(self) -> list[int]:
|
|
159
|
+
seen: dict[int, None] = {}
|
|
160
|
+
for agenda in self.agendas:
|
|
161
|
+
for number in agenda.pull_numbers:
|
|
162
|
+
seen.setdefault(number, None)
|
|
163
|
+
return list(seen)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
# ---------------------------------------------------------------------------
|
|
167
|
+
# 会议纪要
|
|
168
|
+
# ---------------------------------------------------------------------------
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def parse_pad(text: str) -> list[MeetingDraft]:
|
|
172
|
+
"""把整页 Etherpad 文本切成按时间倒序的会议列表。
|
|
173
|
+
|
|
174
|
+
切分依据是会议标题行,而不是分隔线 —— 分隔线是人工画的,
|
|
175
|
+
有的会议没画(实测 17 场里就有),靠它切会漏。
|
|
176
|
+
"""
|
|
177
|
+
lines = text.splitlines()
|
|
178
|
+
starts = [index for index, line in enumerate(lines) if _MEETING_HEADER.match(line)]
|
|
179
|
+
if not starts:
|
|
180
|
+
return []
|
|
181
|
+
|
|
182
|
+
# 末尾补一行长度作为最后一场的右界
|
|
183
|
+
bounds = [*starts, len(lines)]
|
|
184
|
+
meetings: list[MeetingDraft] = []
|
|
185
|
+
for begin, end in pairwise(bounds):
|
|
186
|
+
draft = _parse_meeting(lines[begin:end])
|
|
187
|
+
if draft is not None:
|
|
188
|
+
meetings.append(draft)
|
|
189
|
+
return meetings
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def _parse_meeting(block: list[str]) -> MeetingDraft | None:
|
|
193
|
+
header = _MEETING_HEADER.match(block[0])
|
|
194
|
+
if header is None:
|
|
195
|
+
return None
|
|
196
|
+
|
|
197
|
+
year, month, day, time_text, title = header.groups()
|
|
198
|
+
try:
|
|
199
|
+
meeting_date = date(int(year), int(month), int(day))
|
|
200
|
+
except ValueError:
|
|
201
|
+
# 手写日期会有 2/30 这种不存在的值。丢掉一场,不要丢掉整页。
|
|
202
|
+
return None
|
|
203
|
+
|
|
204
|
+
draft = MeetingDraft(date=meeting_date, time=time_text, title=title.strip())
|
|
205
|
+
|
|
206
|
+
section = ""
|
|
207
|
+
agenda: AgendaDraft | None = None
|
|
208
|
+
legacy_lines: list[str] = []
|
|
209
|
+
body: list[str] = []
|
|
210
|
+
|
|
211
|
+
def commit(current: AgendaDraft | None, lines: list[str]) -> None:
|
|
212
|
+
if current is None:
|
|
213
|
+
return
|
|
214
|
+
_finish_agenda(current, lines)
|
|
215
|
+
if not _is_template(current):
|
|
216
|
+
draft.agendas.append(current)
|
|
217
|
+
|
|
218
|
+
for line in block[1:]:
|
|
219
|
+
if draft.host is None and (match := _HOST.match(line)):
|
|
220
|
+
draft.host = match.group(1).strip()
|
|
221
|
+
if order := _HOST_ORDER.search(line):
|
|
222
|
+
draft.host_order = [
|
|
223
|
+
part.strip() for part in re.split(r"->|→", order.group(1)) if part.strip()
|
|
224
|
+
]
|
|
225
|
+
continue
|
|
226
|
+
if draft.next_host is None and (match := _NEXT_HOST.match(line)):
|
|
227
|
+
draft.next_host = match.group(1).strip()
|
|
228
|
+
continue
|
|
229
|
+
if draft.meeting_url is None and (match := _MEETING_URL.match(line)):
|
|
230
|
+
draft.meeting_url = match.group(1)
|
|
231
|
+
continue
|
|
232
|
+
if draft.minutes_url is None and (match := _MINUTES_URL.match(line)):
|
|
233
|
+
draft.minutes_url = match.group(1)
|
|
234
|
+
continue
|
|
235
|
+
|
|
236
|
+
if match := _SECTION.match(line):
|
|
237
|
+
commit(agenda, body)
|
|
238
|
+
agenda, body = None, []
|
|
239
|
+
section = match.group(2).strip()
|
|
240
|
+
continue
|
|
241
|
+
|
|
242
|
+
if match := _AGENDA.match(line):
|
|
243
|
+
commit(agenda, body)
|
|
244
|
+
agenda = _split_agenda(match.group(1), match.group(2))
|
|
245
|
+
body = []
|
|
246
|
+
continue
|
|
247
|
+
|
|
248
|
+
if agenda is not None:
|
|
249
|
+
body.append(line)
|
|
250
|
+
elif "遗留" in section:
|
|
251
|
+
legacy_lines.append(line)
|
|
252
|
+
|
|
253
|
+
commit(agenda, body)
|
|
254
|
+
draft.legacy = "\n".join(legacy_lines).strip()
|
|
255
|
+
return draft
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def _split_agenda(num: str, rest: str) -> AgendaDraft:
|
|
259
|
+
"""拆出「标题(汇报人)」。中英文括号与全角空格都出现过。"""
|
|
260
|
+
rest = rest.strip()
|
|
261
|
+
owner = None
|
|
262
|
+
match = re.search(r"[((]\s*([^(())]+?)\s*[))]\s*$", rest)
|
|
263
|
+
if match:
|
|
264
|
+
owner = match.group(1).strip()
|
|
265
|
+
rest = rest[: match.start()].strip()
|
|
266
|
+
return AgendaDraft(num=num.strip(), title=rest, owner=owner, body="")
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def _is_template(agenda: AgendaDraft) -> bool:
|
|
270
|
+
"""模板占位议题。"""
|
|
271
|
+
return any(marker in agenda.title for marker in _TEMPLATE_MARKERS)
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def _finish_agenda(agenda: AgendaDraft, body: list[str]) -> None:
|
|
275
|
+
agenda.body = "\n".join(body).strip()
|
|
276
|
+
for match in _LINK.finditer(agenda.body):
|
|
277
|
+
kind, number = match.group(1).lower(), int(match.group(2))
|
|
278
|
+
if kind.startswith("issue"):
|
|
279
|
+
agenda.issue_numbers.append(number)
|
|
280
|
+
else:
|
|
281
|
+
agenda.pull_numbers.append(number)
|
|
282
|
+
|
|
283
|
+
# 只有「进展update」这类议题才带版本报告
|
|
284
|
+
if "进展" in agenda.title or "update" in agenda.title.lower():
|
|
285
|
+
agenda.progress = parse_progress_report(agenda.body)
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
# ---------------------------------------------------------------------------
|
|
289
|
+
# 版本进展报告
|
|
290
|
+
# ---------------------------------------------------------------------------
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def parse_progress_report(text: str) -> list[BranchProgress]:
|
|
294
|
+
"""从议题正文里抽出各分支的合入报告。
|
|
295
|
+
|
|
296
|
+
用一个显式状态机而不是正则一把梭:正文里 ``xxx:`` 形态的行太多,
|
|
297
|
+
补丁标题本身常含冒号(``nfs: use ...``),靠单条正则区分分组标题和
|
|
298
|
+
补丁标题必然互相误伤。
|
|
299
|
+
"""
|
|
300
|
+
blocks: list[BranchProgress] = []
|
|
301
|
+
current: BranchProgress | None = None
|
|
302
|
+
group: ProgressGroup | None = None
|
|
303
|
+
mode = "" # "" | "iso" | "cve"
|
|
304
|
+
|
|
305
|
+
pending_title: str | None = None
|
|
306
|
+
|
|
307
|
+
def flush_item(url: str | None = None) -> None:
|
|
308
|
+
nonlocal pending_title
|
|
309
|
+
if pending_title is None:
|
|
310
|
+
return
|
|
311
|
+
if current is not None and group is not None:
|
|
312
|
+
group.items.append(
|
|
313
|
+
ProgressItem(
|
|
314
|
+
title=pending_title,
|
|
315
|
+
pull_number=_pull_number(url) if url else None,
|
|
316
|
+
url=url,
|
|
317
|
+
)
|
|
318
|
+
)
|
|
319
|
+
pending_title = None
|
|
320
|
+
|
|
321
|
+
for raw in text.splitlines():
|
|
322
|
+
line = raw.rstrip()
|
|
323
|
+
stripped = line.strip()
|
|
324
|
+
|
|
325
|
+
if match := _PROGRESS_HEADER.match(line):
|
|
326
|
+
flush_item()
|
|
327
|
+
current = BranchProgress(branch=normalize_branch_name(match.group(1)))
|
|
328
|
+
blocks.append(current)
|
|
329
|
+
group = None
|
|
330
|
+
mode = ""
|
|
331
|
+
continue
|
|
332
|
+
|
|
333
|
+
if current is None:
|
|
334
|
+
continue
|
|
335
|
+
|
|
336
|
+
if not stripped:
|
|
337
|
+
continue
|
|
338
|
+
|
|
339
|
+
# 补丁标题的下一行是缩进 URL
|
|
340
|
+
if (match := _INDENTED_URL.match(line)) and pending_title is not None:
|
|
341
|
+
flush_item(match.group(1))
|
|
342
|
+
continue
|
|
343
|
+
if _TRUNCATED_LINK.search(stripped):
|
|
344
|
+
# 编号被截断的链接:保留标题,但不猜编号
|
|
345
|
+
flush_item(None)
|
|
346
|
+
continue
|
|
347
|
+
|
|
348
|
+
flush_item()
|
|
349
|
+
|
|
350
|
+
if match := _TAG_UPDATE.search(stripped):
|
|
351
|
+
current.to_tag = match.group(1)
|
|
352
|
+
current.patch_count = int(match.group(2))
|
|
353
|
+
mode = ""
|
|
354
|
+
continue
|
|
355
|
+
if match := _GIT_RANGE.search(stripped):
|
|
356
|
+
current.from_tag = match.group(1)
|
|
357
|
+
current.to_tag = match.group(2)
|
|
358
|
+
mode = ""
|
|
359
|
+
continue
|
|
360
|
+
if _ISO_SECTION.match(stripped):
|
|
361
|
+
mode = "iso"
|
|
362
|
+
group = None
|
|
363
|
+
continue
|
|
364
|
+
if match := _CVE_SECTION.match(stripped):
|
|
365
|
+
mode = "cve"
|
|
366
|
+
group = None
|
|
367
|
+
continue
|
|
368
|
+
if stripped in ("CVE", "CVE:"):
|
|
369
|
+
mode = "cve"
|
|
370
|
+
group = None
|
|
371
|
+
continue
|
|
372
|
+
|
|
373
|
+
if mode == "iso":
|
|
374
|
+
# 镜像链接有时不带缩进
|
|
375
|
+
if "://" in stripped:
|
|
376
|
+
current.iso_urls.append(stripped)
|
|
377
|
+
continue
|
|
378
|
+
mode = ""
|
|
379
|
+
|
|
380
|
+
if mode == "cve":
|
|
381
|
+
found = _CVE_ID.findall(stripped)
|
|
382
|
+
if found:
|
|
383
|
+
current.cve_ids.extend(found)
|
|
384
|
+
# CVE 清单可能跨多行
|
|
385
|
+
continue
|
|
386
|
+
mode = ""
|
|
387
|
+
|
|
388
|
+
if match := _PROGRESS_GROUP.match(stripped):
|
|
389
|
+
name, count = match.group(1).strip(), match.group(2)
|
|
390
|
+
if not any(token in name for token in _NOT_A_GROUP) and "://" not in name:
|
|
391
|
+
group = ProgressGroup(name=name, count=int(count) if count else None)
|
|
392
|
+
current.groups.append(group)
|
|
393
|
+
continue
|
|
394
|
+
|
|
395
|
+
pending_title = stripped
|
|
396
|
+
|
|
397
|
+
flush_item()
|
|
398
|
+
return blocks
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
def _pull_number(url: str | None) -> int | None:
|
|
402
|
+
if not url:
|
|
403
|
+
return None
|
|
404
|
+
match = _LINK.search(url)
|
|
405
|
+
if match is None:
|
|
406
|
+
return None
|
|
407
|
+
kind, number = match.group(1).lower(), int(match.group(2))
|
|
408
|
+
return number if not kind.startswith("issue") else None
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
"""版本与分支的归类。
|
|
2
|
+
|
|
3
|
+
openEuler 内核分两条线(见 sig/Kernel/README.md):创新版本追求新特性验证,
|
|
4
|
+
稳定版本(LTS / OLK)质量优先、维护期长。两条线的评审标准不同,
|
|
5
|
+
混在一个列表里维护者没法按线分流,所以这里先把分支名解释成结构化事实。
|
|
6
|
+
|
|
7
|
+
命名从名称能推出来的部分由代码推,推不出来的(生命周期、上游基线)留给
|
|
8
|
+
``ksp_release_branch`` 登记表由人填 —— 猜测版本基线只会造出一个看起来
|
|
9
|
+
权威、实际是编造的字段。
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import re
|
|
15
|
+
from dataclasses import dataclass
|
|
16
|
+
from enum import StrEnum
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class BranchKind(StrEnum):
|
|
20
|
+
"""分支所属的产品线。"""
|
|
21
|
+
|
|
22
|
+
OLK = "olk"
|
|
23
|
+
LTS = "lts"
|
|
24
|
+
INNOVATION = "innovation"
|
|
25
|
+
OTHER = "other"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
# OLK-6.6 / OLK-5.10
|
|
29
|
+
_OLK = re.compile(r"^OLK-(\d+)\.(\d+)$", re.IGNORECASE)
|
|
30
|
+
# openEuler-24.03-LTS、openEuler-22.03-LTS-SP4、openEuler-1.0-LTS
|
|
31
|
+
# 版本号不限定两位:1.0-LTS 是真实存在的分支,而且 PR 量最大的一条
|
|
32
|
+
_LTS = re.compile(r"^openEuler-(\d+\.\d+)-LTS(?:-SP(\d+))?$", re.IGNORECASE)
|
|
33
|
+
# openEuler-26.09 这类 yy.mm 且非 LTS 的即创新版本
|
|
34
|
+
_INNOVATION = re.compile(r"^openEuler-(\d{2})\.(\d{2})$", re.IGNORECASE)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass(frozen=True)
|
|
38
|
+
class BranchSpec:
|
|
39
|
+
"""分支名的结构化解释。"""
|
|
40
|
+
|
|
41
|
+
name: str
|
|
42
|
+
"""规范化后的名字。用于分组与展示。"""
|
|
43
|
+
|
|
44
|
+
kind: BranchKind
|
|
45
|
+
raw: str = ""
|
|
46
|
+
"""上游原始名字。用于回查数据 —— 库里的 ``target_branch`` 存的就是它。"""
|
|
47
|
+
|
|
48
|
+
series: str | None = None
|
|
49
|
+
"""发行版系列,如 ``24.03-LTS``。OLK 分支没有系列。"""
|
|
50
|
+
|
|
51
|
+
sp: int | None = None
|
|
52
|
+
"""Service Pack 序号。"""
|
|
53
|
+
|
|
54
|
+
upstream_base: str | None = None
|
|
55
|
+
"""上游内核基线,仅 OLK 分支可从名字推得。"""
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def normalize_branch_name(name: str) -> str:
|
|
59
|
+
"""归一化分支名,只处理能确定等价的差异。
|
|
60
|
+
|
|
61
|
+
实测数据里同一个交付有四种写法混用:
|
|
62
|
+
|
|
63
|
+
openEuler-24.03-LTS-SP1 openEuler-24.03-LTS_SP1
|
|
64
|
+
openEuler-24.03-LTS openEuler-24.03_LTS
|
|
65
|
+
|
|
66
|
+
不归一化会让同一个版本在统计里裂成两行,每个版本页的合入数都偏小。
|
|
67
|
+
|
|
68
|
+
只统一 ``LTS`` 与 ``SP\\d+`` 前后的分隔符与大小写。其余部分原样保留:
|
|
69
|
+
分支名大小写敏感,改错了会指向不存在的引用,因此不做泛化的小写化。
|
|
70
|
+
"""
|
|
71
|
+
if not name:
|
|
72
|
+
return name
|
|
73
|
+
normalized = name.strip()
|
|
74
|
+
normalized = re.sub(r"[-_](lts)(?=[-_]|$)", "-LTS", normalized, flags=re.IGNORECASE)
|
|
75
|
+
normalized = re.sub(r"[-_](sp)(\d+)", r"-SP\2", normalized, flags=re.IGNORECASE)
|
|
76
|
+
return normalized
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def classify_branch(name: str) -> BranchSpec:
|
|
80
|
+
"""把分支名解释成产品线、系列与 SP 序号。
|
|
81
|
+
|
|
82
|
+
认不出来的分支不报错:openEuler 会开临时分支,且分支名是外部输入。
|
|
83
|
+
归到 ``OTHER`` 后仍会出现在分支页,只是不带产品线标签。
|
|
84
|
+
"""
|
|
85
|
+
raw = name.strip() if name else name
|
|
86
|
+
normalized = normalize_branch_name(name)
|
|
87
|
+
|
|
88
|
+
if match := _OLK.match(normalized):
|
|
89
|
+
version = f"{match.group(1)}.{match.group(2)}"
|
|
90
|
+
return BranchSpec(
|
|
91
|
+
name=normalized,
|
|
92
|
+
kind=BranchKind.OLK,
|
|
93
|
+
raw=raw,
|
|
94
|
+
upstream_base=version,
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
if match := _LTS.match(normalized):
|
|
98
|
+
version, sp = match.groups()
|
|
99
|
+
return BranchSpec(
|
|
100
|
+
name=normalized,
|
|
101
|
+
kind=BranchKind.LTS,
|
|
102
|
+
raw=raw,
|
|
103
|
+
series=f"{version}-LTS",
|
|
104
|
+
sp=int(sp) if sp else None,
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
if match := _INNOVATION.match(normalized):
|
|
108
|
+
return BranchSpec(
|
|
109
|
+
name=normalized,
|
|
110
|
+
kind=BranchKind.INNOVATION,
|
|
111
|
+
raw=raw,
|
|
112
|
+
series=match.group(0).removeprefix("openEuler-"),
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
return BranchSpec(name=normalized, kind=BranchKind.OTHER, raw=raw)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
# ---------------------------------------------------------------------------
|
|
119
|
+
# 内核 tag
|
|
120
|
+
# ---------------------------------------------------------------------------
|
|
121
|
+
|
|
122
|
+
# 6.6.0-170.0.0 / 5.10.0-331.0.0 / 6.6.0-165
|
|
123
|
+
_KERNEL_TAG = re.compile(r"^(\d+\.\d+)\.(\d+)-(\d+)(?:\.\d+)*$")
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
@dataclass(frozen=True)
|
|
127
|
+
class KernelTag:
|
|
128
|
+
base: str
|
|
129
|
+
"""上游基线,如 ``6.6``。"""
|
|
130
|
+
|
|
131
|
+
build: int
|
|
132
|
+
"""构建号,如 170。同一基线下单调递增。"""
|
|
133
|
+
|
|
134
|
+
raw: str
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def parse_kernel_tag(tag: str) -> KernelTag | None:
|
|
138
|
+
"""解析 ``6.6.0-170.0.0`` 形态的 tag。
|
|
139
|
+
|
|
140
|
+
构建号是判断"两个 tag 之间合入了多少"的锚点:会议纪要里
|
|
141
|
+
``6.6.0-167.0.0..6.6.0-170.0.0`` 就是靠它界定的区间。
|
|
142
|
+
"""
|
|
143
|
+
match = _KERNEL_TAG.match(tag.strip())
|
|
144
|
+
if not match:
|
|
145
|
+
return None
|
|
146
|
+
return KernelTag(base=match.group(1), build=int(match.group(3)), raw=tag.strip())
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def compare_tags(left: str, right: str) -> int | None:
|
|
150
|
+
"""按 (基线, 构建号) 比较两个 tag。无法解析时返回 None。
|
|
151
|
+
|
|
152
|
+
不能按字符串比:``6.6.0-99.0.0`` 字典序小于 ``6.6.0-170.0.0``,
|
|
153
|
+
但 9 与 170 的大小关系取决于先比构建号还是先比文本 —— 只有解析成
|
|
154
|
+
整数才不会在构建号跨过位数时翻车。
|
|
155
|
+
"""
|
|
156
|
+
left_tag, right_tag = parse_kernel_tag(left), parse_kernel_tag(right)
|
|
157
|
+
if left_tag is None or right_tag is None:
|
|
158
|
+
return None
|
|
159
|
+
if left_tag.base != right_tag.base:
|
|
160
|
+
return None
|
|
161
|
+
if left_tag.build == right_tag.build:
|
|
162
|
+
return 0
|
|
163
|
+
return 1 if left_tag.build > right_tag.build else -1
|