@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,288 @@
|
|
|
1
|
+
"""Pull Request 及其关联数据。"""
|
|
2
|
+
|
|
3
|
+
import uuid
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from enum import StrEnum
|
|
6
|
+
|
|
7
|
+
from sqlalchemy import (
|
|
8
|
+
BigInteger,
|
|
9
|
+
Boolean,
|
|
10
|
+
DateTime,
|
|
11
|
+
Enum,
|
|
12
|
+
ForeignKey,
|
|
13
|
+
Index,
|
|
14
|
+
Integer,
|
|
15
|
+
String,
|
|
16
|
+
Text,
|
|
17
|
+
UniqueConstraint,
|
|
18
|
+
func,
|
|
19
|
+
true,
|
|
20
|
+
)
|
|
21
|
+
from sqlalchemy.dialects.postgresql import ARRAY, JSONB, UUID
|
|
22
|
+
from sqlalchemy.orm import Mapped, mapped_column
|
|
23
|
+
|
|
24
|
+
from app.domain.review import EventType as ReviewEventType
|
|
25
|
+
from app.models.base import Base, TimestampMixin
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class PRState(StrEnum):
|
|
29
|
+
OPEN = "open"
|
|
30
|
+
CLOSED = "closed"
|
|
31
|
+
MERGED = "merged"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class PullRequest(Base, TimestampMixin):
|
|
35
|
+
__tablename__ = "ksp_pull_request"
|
|
36
|
+
__table_args__ = (
|
|
37
|
+
UniqueConstraint("repository_id", "number", name="uq_ksp_pull_request_repo_number"),
|
|
38
|
+
Index("ix_ksp_pull_request_repo_state_updated", "repository_id", "state", "updated_at"),
|
|
39
|
+
Index("ix_ksp_pull_request_label_names", "label_names", postgresql_using="gin"),
|
|
40
|
+
Index("ix_ksp_pull_request_repo_synced", "repository_id", "detail_synced_at"),
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
|
44
|
+
repository_id: Mapped[uuid.UUID] = mapped_column(
|
|
45
|
+
UUID(as_uuid=True),
|
|
46
|
+
ForeignKey("ksp_repository.id", ondelete="CASCADE"),
|
|
47
|
+
nullable=False,
|
|
48
|
+
index=True,
|
|
49
|
+
)
|
|
50
|
+
number: Mapped[int] = mapped_column(Integer, nullable=False)
|
|
51
|
+
|
|
52
|
+
title: Mapped[str] = mapped_column(Text, nullable=False)
|
|
53
|
+
body: Mapped[str | None] = mapped_column(Text, nullable=True)
|
|
54
|
+
state: Mapped[PRState] = mapped_column(
|
|
55
|
+
Enum(PRState, name="pull_state", values_callable=lambda e: [m.value for m in e]),
|
|
56
|
+
nullable=False,
|
|
57
|
+
index=True,
|
|
58
|
+
)
|
|
59
|
+
draft: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
|
|
60
|
+
|
|
61
|
+
author_login: Mapped[str | None] = mapped_column(String(128), nullable=True, index=True)
|
|
62
|
+
author_name: Mapped[str | None] = mapped_column(String(255), nullable=True)
|
|
63
|
+
author_avatar_url: Mapped[str | None] = mapped_column(String(512), nullable=True)
|
|
64
|
+
|
|
65
|
+
source_branch: Mapped[str | None] = mapped_column(String(255), nullable=True)
|
|
66
|
+
target_branch: Mapped[str | None] = mapped_column(String(255), nullable=True, index=True)
|
|
67
|
+
|
|
68
|
+
# 原始标签快照 + 便于 GIN 索引查询的扁平数组
|
|
69
|
+
labels: Mapped[list | None] = mapped_column(JSONB, nullable=True)
|
|
70
|
+
label_names: Mapped[list[str] | None] = mapped_column(ARRAY(Text), nullable=True)
|
|
71
|
+
|
|
72
|
+
added_lines: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
|
|
73
|
+
removed_lines: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
|
|
74
|
+
changed_files: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
|
|
75
|
+
# AtomGit 的 notes 字段直接给出评论数,是变更检测的信号之一
|
|
76
|
+
comments_count: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
|
|
77
|
+
|
|
78
|
+
mergeable: Mapped[bool | None] = mapped_column(Boolean, nullable=True)
|
|
79
|
+
merge_conflict: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
|
|
80
|
+
merge_check_detail: Mapped[dict | None] = mapped_column(JSONB, nullable=True)
|
|
81
|
+
|
|
82
|
+
# 门禁阶段。由 label_names 推导后落库,属于刻意的反规范化:
|
|
83
|
+
# 列表页需要按"待评审/可合入"筛选并排序,1000+ 条记录不可能在应用层
|
|
84
|
+
# 逐条推导再分页。每次列表同步时重算,来源与推导逻辑唯一(domain.review)。
|
|
85
|
+
gate_stage: Mapped[str] = mapped_column(
|
|
86
|
+
String(32), nullable=False, default="cla_pending", server_default="cla_pending", index=True
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
atomgit_created_at: Mapped[datetime | None] = mapped_column(
|
|
90
|
+
DateTime(timezone=True), nullable=True
|
|
91
|
+
)
|
|
92
|
+
atomgit_updated_at: Mapped[datetime | None] = mapped_column(
|
|
93
|
+
DateTime(timezone=True), nullable=True
|
|
94
|
+
)
|
|
95
|
+
merged_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
|
|
96
|
+
closed_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
|
|
97
|
+
|
|
98
|
+
html_url: Mapped[str | None] = mapped_column(String(512), nullable=True)
|
|
99
|
+
|
|
100
|
+
first_seen_at: Mapped[datetime] = mapped_column(
|
|
101
|
+
DateTime(timezone=True), server_default=func.now(), nullable=False
|
|
102
|
+
)
|
|
103
|
+
last_synced_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
|
|
104
|
+
detail_synced_at: Mapped[datetime | None] = mapped_column(
|
|
105
|
+
DateTime(timezone=True), nullable=True
|
|
106
|
+
)
|
|
107
|
+
# 内容变化后置位,详情同步处理后清除。
|
|
108
|
+
# 不能用「last_synced_at > detail_synced_at」代替 —— 列表同步每次都更新
|
|
109
|
+
# last_synced_at,会让全部 PR 恒久处于"待拉详情"状态,每轮产生数千次请求。
|
|
110
|
+
needs_detail: Mapped[bool] = mapped_column(
|
|
111
|
+
Boolean, nullable=False, default=True, server_default=true()
|
|
112
|
+
)
|
|
113
|
+
# 内容指纹:只有变化才需要重新触发 AI 分析
|
|
114
|
+
content_hash: Mapped[str | None] = mapped_column(String(64), nullable=True, index=True)
|
|
115
|
+
|
|
116
|
+
def __repr__(self) -> str:
|
|
117
|
+
return f"<PullRequest #{self.number} {self.state.value}>"
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
class PRCommit(Base):
|
|
121
|
+
"""PR 包含的提交。序号对应补丁在 PR 中的顺序(0001、0002…)。"""
|
|
122
|
+
|
|
123
|
+
__tablename__ = "ksp_pr_commit"
|
|
124
|
+
__table_args__ = (UniqueConstraint("pull_request_id", "sha", name="uq_ksp_pr_commit_pr_sha"),)
|
|
125
|
+
|
|
126
|
+
id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=True)
|
|
127
|
+
pull_request_id: Mapped[uuid.UUID] = mapped_column(
|
|
128
|
+
UUID(as_uuid=True),
|
|
129
|
+
ForeignKey("ksp_pull_request.id", ondelete="CASCADE"),
|
|
130
|
+
nullable=False,
|
|
131
|
+
index=True,
|
|
132
|
+
)
|
|
133
|
+
sha: Mapped[str] = mapped_column(String(64), nullable=False)
|
|
134
|
+
sequence: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
|
|
135
|
+
|
|
136
|
+
subject: Mapped[str] = mapped_column(Text, nullable=False, default="")
|
|
137
|
+
message: Mapped[str] = mapped_column(Text, nullable=False, default="")
|
|
138
|
+
author_name: Mapped[str | None] = mapped_column(String(255), nullable=True)
|
|
139
|
+
author_email: Mapped[str | None] = mapped_column(String(255), nullable=True)
|
|
140
|
+
committed_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
|
|
141
|
+
|
|
142
|
+
# 补丁格式审查的落点:openEuler 要求 Signed-off-by 链完整
|
|
143
|
+
signed_off_bys: Mapped[list[str] | None] = mapped_column(JSONB, nullable=True)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
class PRFile(Base):
|
|
147
|
+
"""PR 变更文件。patch 原文保留,供后续逐补丁审查与 backport 对比。"""
|
|
148
|
+
|
|
149
|
+
__tablename__ = "ksp_pr_file"
|
|
150
|
+
__table_args__ = (
|
|
151
|
+
UniqueConstraint("pull_request_id", "filename", name="uq_ksp_pr_file_pr_filename"),
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=True)
|
|
155
|
+
pull_request_id: Mapped[uuid.UUID] = mapped_column(
|
|
156
|
+
UUID(as_uuid=True),
|
|
157
|
+
ForeignKey("ksp_pull_request.id", ondelete="CASCADE"),
|
|
158
|
+
nullable=False,
|
|
159
|
+
index=True,
|
|
160
|
+
)
|
|
161
|
+
filename: Mapped[str] = mapped_column(Text, nullable=False)
|
|
162
|
+
status: Mapped[str | None] = mapped_column(String(32), nullable=True)
|
|
163
|
+
additions: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
|
|
164
|
+
deletions: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
|
|
165
|
+
|
|
166
|
+
# 内核仓库禁止提交二进制文件,此标记直接支撑该规则的自动判定
|
|
167
|
+
is_binary: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
|
|
168
|
+
too_large: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
|
|
169
|
+
diff: Mapped[str | None] = mapped_column(Text, nullable=True)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
class PRComment(Base):
|
|
173
|
+
"""PR 评论原文。
|
|
174
|
+
|
|
175
|
+
评论此前只被解析成评审事件就丢弃,界面上无从看到讨论内容。
|
|
176
|
+
但评审意见、维护者的提问、以及 openEuler 机器人自动产生的变更摘要
|
|
177
|
+
与代码审查结论都只存在于评论里,因此原文必须留存并展示。
|
|
178
|
+
|
|
179
|
+
以 AtomGit 的评论 id 为唯一键,重复同步不会产生重复行。
|
|
180
|
+
"""
|
|
181
|
+
|
|
182
|
+
__tablename__ = "ksp_pr_comment"
|
|
183
|
+
__table_args__ = (
|
|
184
|
+
UniqueConstraint("pull_request_id", "comment_id", name="uq_ksp_pr_comment_identity"),
|
|
185
|
+
# 评论按时间正序展示,因此按 (PR, 上游创建时间) 建索引
|
|
186
|
+
Index("ix_ksp_pr_comment_pr_created", "pull_request_id", "atomgit_created_at"),
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=True)
|
|
190
|
+
pull_request_id: Mapped[uuid.UUID] = mapped_column(
|
|
191
|
+
UUID(as_uuid=True),
|
|
192
|
+
ForeignKey("ksp_pull_request.id", ondelete="CASCADE"),
|
|
193
|
+
nullable=False,
|
|
194
|
+
index=True,
|
|
195
|
+
)
|
|
196
|
+
# AtomGit 侧的评论 id。以字符串存储:上游 id 类型不稳定(实测有整数,
|
|
197
|
+
# 也有形如 "note_123" 的字符串),转成整数会在遇到后者时直接崩
|
|
198
|
+
comment_id: Mapped[str] = mapped_column(String(64), nullable=False)
|
|
199
|
+
# 行内评论会带上被评论的文件;整体评论为空
|
|
200
|
+
comment_type: Mapped[str | None] = mapped_column(String(32), nullable=True)
|
|
201
|
+
discussion_id: Mapped[str | None] = mapped_column(String(64), nullable=True)
|
|
202
|
+
|
|
203
|
+
author_login: Mapped[str] = mapped_column(String(128), nullable=False, default="")
|
|
204
|
+
author_avatar_url: Mapped[str | None] = mapped_column(String(512), nullable=True)
|
|
205
|
+
body: Mapped[str] = mapped_column(Text, nullable=False, default="")
|
|
206
|
+
|
|
207
|
+
atomgit_created_at: Mapped[datetime | None] = mapped_column(
|
|
208
|
+
DateTime(timezone=True), nullable=True
|
|
209
|
+
)
|
|
210
|
+
atomgit_updated_at: Mapped[datetime | None] = mapped_column(
|
|
211
|
+
DateTime(timezone=True), nullable=True
|
|
212
|
+
)
|
|
213
|
+
last_synced_at: Mapped[datetime | None] = mapped_column(
|
|
214
|
+
DateTime(timezone=True), nullable=True
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
def __repr__(self) -> str:
|
|
218
|
+
return f"<PRComment {self.comment_id} by={self.author_login}>"
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
class ReviewEventSource(StrEnum):
|
|
222
|
+
LABEL = "label"
|
|
223
|
+
COMMENT = "comment"
|
|
224
|
+
WEBHOOK = "webhook"
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
class ReviewEvent(Base):
|
|
228
|
+
"""结构化的评审事件。
|
|
229
|
+
|
|
230
|
+
这是评审子系统的基石 —— AtomGit 把评审状态承载在标签与评论里,
|
|
231
|
+
必须解析成结构化事件,才能计算评审 SLA、评审人工作量与等待时长。
|
|
232
|
+
|
|
233
|
+
唯一约束保证幂等:重复同步不会产生重复事件。
|
|
234
|
+
"""
|
|
235
|
+
|
|
236
|
+
__tablename__ = "ksp_review_event"
|
|
237
|
+
__table_args__ = (
|
|
238
|
+
UniqueConstraint(
|
|
239
|
+
"pull_request_id",
|
|
240
|
+
"event_type",
|
|
241
|
+
"actor_login",
|
|
242
|
+
"occurred_at",
|
|
243
|
+
name="uq_ksp_review_event_identity",
|
|
244
|
+
),
|
|
245
|
+
Index("ix_ksp_review_event_pr_occurred", "pull_request_id", "occurred_at"),
|
|
246
|
+
Index("ix_ksp_review_event_actor_occurred", "actor_login", "occurred_at"),
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=True)
|
|
250
|
+
pull_request_id: Mapped[uuid.UUID] = mapped_column(
|
|
251
|
+
UUID(as_uuid=True),
|
|
252
|
+
ForeignKey("ksp_pull_request.id", ondelete="CASCADE"),
|
|
253
|
+
nullable=False,
|
|
254
|
+
index=True,
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
event_type: Mapped[ReviewEventType] = mapped_column(
|
|
258
|
+
Enum(
|
|
259
|
+
ReviewEventType,
|
|
260
|
+
name="review_event_type",
|
|
261
|
+
values_callable=lambda e: [m.value for m in e],
|
|
262
|
+
),
|
|
263
|
+
nullable=False,
|
|
264
|
+
index=True,
|
|
265
|
+
)
|
|
266
|
+
actor_login: Mapped[str] = mapped_column(String(128), nullable=False, default="", index=True)
|
|
267
|
+
# 若能匹配到平台用户则关联,用于归属评审工作量
|
|
268
|
+
actor_user_id: Mapped[uuid.UUID | None] = mapped_column(
|
|
269
|
+
UUID(as_uuid=True), ForeignKey("ksp_user.id", ondelete="SET NULL"), nullable=True
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
source: Mapped[ReviewEventSource] = mapped_column(
|
|
273
|
+
Enum(
|
|
274
|
+
ReviewEventSource,
|
|
275
|
+
name="review_event_source",
|
|
276
|
+
values_callable=lambda e: [m.value for m in e],
|
|
277
|
+
),
|
|
278
|
+
nullable=False,
|
|
279
|
+
)
|
|
280
|
+
# 原始证据(标签名或评论片段),解析规则演进时可回溯
|
|
281
|
+
raw_payload: Mapped[dict | None] = mapped_column(JSONB, nullable=True)
|
|
282
|
+
occurred_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
|
|
283
|
+
created_at: Mapped[datetime] = mapped_column(
|
|
284
|
+
DateTime(timezone=True), server_default=func.now(), nullable=False
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
def __repr__(self) -> str:
|
|
288
|
+
return f"<ReviewEvent {self.event_type.value} by={self.actor_login}>"
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
"""纳管仓库与同步游标。"""
|
|
2
|
+
|
|
3
|
+
import uuid
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from enum import StrEnum
|
|
6
|
+
|
|
7
|
+
from sqlalchemy import (
|
|
8
|
+
BigInteger,
|
|
9
|
+
Boolean,
|
|
10
|
+
DateTime,
|
|
11
|
+
Enum,
|
|
12
|
+
ForeignKey,
|
|
13
|
+
Integer,
|
|
14
|
+
String,
|
|
15
|
+
Text,
|
|
16
|
+
UniqueConstraint,
|
|
17
|
+
func,
|
|
18
|
+
)
|
|
19
|
+
from sqlalchemy.dialects.postgresql import JSONB, UUID
|
|
20
|
+
from sqlalchemy.orm import Mapped, mapped_column
|
|
21
|
+
|
|
22
|
+
from app.models.base import Base, TimestampMixin
|
|
23
|
+
|
|
24
|
+
DEFAULT_API_BASE_URL = "https://api.atomgit.com/api/v5"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class Repository(Base, TimestampMixin):
|
|
28
|
+
__tablename__ = "ksp_repository"
|
|
29
|
+
__table_args__ = (UniqueConstraint("owner", "name", name="uq_ksp_repository_owner_name"),)
|
|
30
|
+
|
|
31
|
+
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
|
32
|
+
owner: Mapped[str] = mapped_column(String(128), nullable=False)
|
|
33
|
+
name: Mapped[str] = mapped_column(String(128), nullable=False)
|
|
34
|
+
display_name: Mapped[str | None] = mapped_column(String(255), nullable=True)
|
|
35
|
+
description: Mapped[str | None] = mapped_column(Text, nullable=True)
|
|
36
|
+
|
|
37
|
+
# 可配置:AtomGit 与 GitCode 共用后端,两者 token 通用
|
|
38
|
+
api_base_url: Mapped[str] = mapped_column(
|
|
39
|
+
String(255), nullable=False, default=DEFAULT_API_BASE_URL
|
|
40
|
+
)
|
|
41
|
+
credential_id: Mapped[uuid.UUID | None] = mapped_column(
|
|
42
|
+
UUID(as_uuid=True), ForeignKey("ksp_credential.id", ondelete="SET NULL"), nullable=True
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
default_branch: Mapped[str | None] = mapped_column(String(128), nullable=True)
|
|
46
|
+
# 关注的分支,如 ["OLK-6.6", "OLK-5.10", "master"]
|
|
47
|
+
tracked_branches: Mapped[list | None] = mapped_column(JSONB, nullable=True)
|
|
48
|
+
|
|
49
|
+
sync_enabled: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True)
|
|
50
|
+
sync_interval_seconds: Mapped[int] = mapped_column(Integer, nullable=False, default=300)
|
|
51
|
+
# 首次同步回填的历史窗口(天)。全量回填 1000+ PR 会耗时过长,
|
|
52
|
+
# 故默认只回填近半年,其余由对账任务渐进补齐。
|
|
53
|
+
backfill_days: Mapped[int] = mapped_column(Integer, nullable=False, default=180)
|
|
54
|
+
|
|
55
|
+
# webhook 无签名头,改用 URL 路径内嵌随机密钥 + IP 白名单
|
|
56
|
+
webhook_secret: Mapped[str | None] = mapped_column(String(64), nullable=True, unique=True)
|
|
57
|
+
|
|
58
|
+
last_sync_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
|
|
59
|
+
last_sync_error: Mapped[str | None] = mapped_column(Text, nullable=True)
|
|
60
|
+
|
|
61
|
+
@property
|
|
62
|
+
def full_name(self) -> str:
|
|
63
|
+
return f"{self.owner}/{self.name}"
|
|
64
|
+
|
|
65
|
+
def __repr__(self) -> str:
|
|
66
|
+
return f"<Repository {self.full_name}>"
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class SyncResource(StrEnum):
|
|
70
|
+
PULLS = "pulls"
|
|
71
|
+
ISSUES = "issues"
|
|
72
|
+
LABELS = "labels"
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class SyncCursor(Base):
|
|
76
|
+
"""每仓库每资源的同步水位。
|
|
77
|
+
|
|
78
|
+
用于增量同步:只处理 updated_at 晚于水位的对象。
|
|
79
|
+
"""
|
|
80
|
+
|
|
81
|
+
__tablename__ = "ksp_sync_cursor"
|
|
82
|
+
__table_args__ = (
|
|
83
|
+
UniqueConstraint("repository_id", "resource", name="uq_ksp_sync_cursor_repo_resource"),
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=True)
|
|
87
|
+
repository_id: Mapped[uuid.UUID] = mapped_column(
|
|
88
|
+
UUID(as_uuid=True), ForeignKey("ksp_repository.id", ondelete="CASCADE"), nullable=False
|
|
89
|
+
)
|
|
90
|
+
resource: Mapped[SyncResource] = mapped_column(
|
|
91
|
+
Enum(
|
|
92
|
+
SyncResource,
|
|
93
|
+
name="sync_resource",
|
|
94
|
+
values_callable=lambda e: [m.value for m in e],
|
|
95
|
+
),
|
|
96
|
+
nullable=False,
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
last_synced_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
|
|
100
|
+
last_seen_updated_at: Mapped[datetime | None] = mapped_column(
|
|
101
|
+
DateTime(timezone=True), nullable=True
|
|
102
|
+
)
|
|
103
|
+
total_seen: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
|
|
104
|
+
# 触发限流后的退避截止时间
|
|
105
|
+
backoff_until: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class SyncStatus(StrEnum):
|
|
109
|
+
RUNNING = "running"
|
|
110
|
+
SUCCEEDED = "succeeded"
|
|
111
|
+
FAILED = "failed"
|
|
112
|
+
PARTIAL = "partial"
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
class SyncTrigger(StrEnum):
|
|
116
|
+
SCHEDULED = "scheduled"
|
|
117
|
+
MANUAL = "manual"
|
|
118
|
+
WEBHOOK = "webhook"
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
class SyncRun(Base):
|
|
122
|
+
"""单次同步执行记录,支撑"同步健康度"面板。"""
|
|
123
|
+
|
|
124
|
+
__tablename__ = "ksp_sync_run"
|
|
125
|
+
|
|
126
|
+
id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=True)
|
|
127
|
+
repository_id: Mapped[uuid.UUID] = mapped_column(
|
|
128
|
+
UUID(as_uuid=True),
|
|
129
|
+
ForeignKey("ksp_repository.id", ondelete="CASCADE"),
|
|
130
|
+
nullable=False,
|
|
131
|
+
index=True,
|
|
132
|
+
)
|
|
133
|
+
resource: Mapped[SyncResource] = mapped_column(
|
|
134
|
+
Enum(
|
|
135
|
+
SyncResource,
|
|
136
|
+
name="sync_resource",
|
|
137
|
+
values_callable=lambda e: [m.value for m in e],
|
|
138
|
+
),
|
|
139
|
+
nullable=False,
|
|
140
|
+
)
|
|
141
|
+
trigger: Mapped[SyncTrigger] = mapped_column(
|
|
142
|
+
Enum(
|
|
143
|
+
SyncTrigger,
|
|
144
|
+
name="sync_trigger",
|
|
145
|
+
values_callable=lambda e: [m.value for m in e],
|
|
146
|
+
),
|
|
147
|
+
nullable=False,
|
|
148
|
+
default=SyncTrigger.SCHEDULED,
|
|
149
|
+
)
|
|
150
|
+
status: Mapped[SyncStatus] = mapped_column(
|
|
151
|
+
Enum(
|
|
152
|
+
SyncStatus,
|
|
153
|
+
name="sync_status",
|
|
154
|
+
values_callable=lambda e: [m.value for m in e],
|
|
155
|
+
),
|
|
156
|
+
nullable=False,
|
|
157
|
+
default=SyncStatus.RUNNING,
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
started_at: Mapped[datetime] = mapped_column(
|
|
161
|
+
DateTime(timezone=True), server_default=func.now(), nullable=False, index=True
|
|
162
|
+
)
|
|
163
|
+
finished_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
|
|
164
|
+
|
|
165
|
+
api_calls: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
|
|
166
|
+
items_fetched: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
|
|
167
|
+
items_created: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
|
|
168
|
+
items_updated: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
|
|
169
|
+
items_failed: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
|
|
170
|
+
error: Mapped[str | None] = mapped_column(Text, nullable=True)
|
|
171
|
+
|
|
172
|
+
@property
|
|
173
|
+
def duration_seconds(self) -> float | None:
|
|
174
|
+
if self.finished_at is None:
|
|
175
|
+
return None
|
|
176
|
+
return (self.finished_at - self.started_at).total_seconds()
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
class WebhookDelivery(Base):
|
|
180
|
+
"""Webhook 投递记录,以 delivery_id 为主键天然实现幂等。
|
|
181
|
+
|
|
182
|
+
重复投递在 INSERT 冲突时即被识别丢弃。
|
|
183
|
+
"""
|
|
184
|
+
|
|
185
|
+
__tablename__ = "ksp_webhook_delivery"
|
|
186
|
+
|
|
187
|
+
delivery_id: Mapped[str] = mapped_column(String(64), primary_key=True)
|
|
188
|
+
repository_id: Mapped[uuid.UUID | None] = mapped_column(
|
|
189
|
+
UUID(as_uuid=True), ForeignKey("ksp_repository.id", ondelete="CASCADE"), nullable=True
|
|
190
|
+
)
|
|
191
|
+
event: Mapped[str] = mapped_column(String(64), nullable=False)
|
|
192
|
+
recognized: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
|
|
193
|
+
received_at: Mapped[datetime] = mapped_column(
|
|
194
|
+
DateTime(timezone=True), server_default=func.now(), nullable=False, index=True
|
|
195
|
+
)
|
|
196
|
+
processed_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
"""SIG 成员与模块归属。
|
|
2
|
+
|
|
3
|
+
三个来源,职责不同,都保留:
|
|
4
|
+
|
|
5
|
+
- **成员名单**来自 ``sig/Kernel/sig-info.yaml`` —— openEuler 的机器可读 SIG
|
|
6
|
+
元数据文件,字段完整(姓名、邮箱、组织、两个平台账号),133 人。
|
|
7
|
+
它才是权威名单:谁有权评审、谁能合入、谁负责哪个分支,以它为准。
|
|
8
|
+
- ``sig/Kernel/README.md`` 与 ``committers.md`` 是给人看的文档,
|
|
9
|
+
README 只列了 4 位维护者的展示名,committers.md 是模块分工表。
|
|
10
|
+
两者只在 yaml 取不到时兜底,不作为主来源。
|
|
11
|
+
- **模块归属**仍来自 ``committers.md`` 的"模块 / 文件 / Committer"表 ——
|
|
12
|
+
yaml 里没有内核路径信息。它把路径映射到具体的 Committer,
|
|
13
|
+
是"这个 PR 该找谁看"的依据。
|
|
14
|
+
|
|
15
|
+
成员表刻意只存名单,不存贡献统计。贡献统计按需从 PR / 评审事件里现算 ——
|
|
16
|
+
存下来就要考虑失效,而名单是低频变化的,两者寿命不同,混在一张表里
|
|
17
|
+
会让一个每天变的数字和一份权威名单互相拖累。
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
import uuid
|
|
21
|
+
from datetime import datetime
|
|
22
|
+
from enum import StrEnum
|
|
23
|
+
|
|
24
|
+
from sqlalchemy import (
|
|
25
|
+
Boolean,
|
|
26
|
+
DateTime,
|
|
27
|
+
ForeignKey,
|
|
28
|
+
Index,
|
|
29
|
+
Integer,
|
|
30
|
+
String,
|
|
31
|
+
Text,
|
|
32
|
+
UniqueConstraint,
|
|
33
|
+
)
|
|
34
|
+
from sqlalchemy.dialects.postgresql import ARRAY, UUID
|
|
35
|
+
from sqlalchemy.orm import Mapped, mapped_column
|
|
36
|
+
|
|
37
|
+
from app.models.base import Base, TimestampMixin
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class MemberRole(StrEnum):
|
|
41
|
+
MAINTAINER = "maintainer"
|
|
42
|
+
COMMITTER = "committer"
|
|
43
|
+
CONTRIBUTOR = "contributor"
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class MemberSource(StrEnum):
|
|
47
|
+
"""名单来源。
|
|
48
|
+
|
|
49
|
+
``SIG_INFO`` 是权威来源(openEuler 的机器可读 SIG 元数据文件),
|
|
50
|
+
其余两个是给人看的文档,只在 yaml 取不到时兜底。
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
SIG_INFO = "sig_info_yaml"
|
|
54
|
+
README = "sig_readme"
|
|
55
|
+
COMMITTERS = "committers_md"
|
|
56
|
+
MANUAL = "manual"
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class SIGMember(Base, TimestampMixin):
|
|
60
|
+
__tablename__ = "ksp_sig_member"
|
|
61
|
+
__table_args__ = (
|
|
62
|
+
Index("ix_ksp_sig_member_role", "role"),
|
|
63
|
+
Index("ix_ksp_sig_member_atomgit", "atomgit_id"),
|
|
64
|
+
Index("ix_ksp_sig_member_email", "email"),
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
|
68
|
+
|
|
69
|
+
name: Mapped[str] = mapped_column(String(128), nullable=False)
|
|
70
|
+
|
|
71
|
+
# 三个平台的登录名。同一人往往只在一部分平台上留名,实测 atomgit_id
|
|
72
|
+
# 覆盖率 86%、gitee_id 88%、github_id 0%,因此都不能设为必填。
|
|
73
|
+
atomgit_id: Mapped[str | None] = mapped_column(String(128), nullable=True)
|
|
74
|
+
gitee_id: Mapped[str | None] = mapped_column(String(128), nullable=True)
|
|
75
|
+
github_id: Mapped[str | None] = mapped_column(String(128), nullable=True)
|
|
76
|
+
email: Mapped[str | None] = mapped_column(String(255), nullable=True)
|
|
77
|
+
|
|
78
|
+
role: Mapped[str] = mapped_column(
|
|
79
|
+
String(16), nullable=False, default=MemberRole.COMMITTER.value
|
|
80
|
+
)
|
|
81
|
+
"""最高角色。一个人可以同时是维护者与某仓库的 committer。"""
|
|
82
|
+
|
|
83
|
+
roles: Mapped[list[str] | None] = mapped_column(ARRAY(Text), nullable=True)
|
|
84
|
+
"""全部角色。只存最高角色会丢掉"他还是某仓库的 committer"这一信息。"""
|
|
85
|
+
|
|
86
|
+
org: Mapped[str | None] = mapped_column(String(128), nullable=True)
|
|
87
|
+
|
|
88
|
+
repositories: Mapped[list[str] | None] = mapped_column(ARRAY(Text), nullable=True)
|
|
89
|
+
"""参与的仓库。"""
|
|
90
|
+
|
|
91
|
+
admin_repositories: Mapped[list[str] | None] = mapped_column(ARRAY(Text), nullable=True)
|
|
92
|
+
"""担任 repo_admin 的仓库。"""
|
|
93
|
+
|
|
94
|
+
kept_branches: Mapped[list[str] | None] = mapped_column(ARRAY(Text), nullable=True)
|
|
95
|
+
"""作为 keeper 负责的分支。分支页据此显示"该分支谁负责"。"""
|
|
96
|
+
|
|
97
|
+
is_active: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True)
|
|
98
|
+
source: Mapped[str] = mapped_column(
|
|
99
|
+
String(24), nullable=False, default=MemberSource.COMMITTERS.value
|
|
100
|
+
)
|
|
101
|
+
source_url: Mapped[str | None] = mapped_column(String(512), nullable=True)
|
|
102
|
+
last_synced_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
|
|
103
|
+
|
|
104
|
+
def __repr__(self) -> str:
|
|
105
|
+
return f"<SIGMember {self.name} {self.role}>"
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class SubsystemOwner(Base, TimestampMixin):
|
|
109
|
+
"""模块 → Committer 的映射,来自 ``committers.md``。
|
|
110
|
+
|
|
111
|
+
``paths`` 是内核路径前缀。上游用 ``mm/``、``net/bpf/`` 这类前缀,
|
|
112
|
+
也用过 ``kernel/*``、``include/linux/kvm*`` 这类通配写法,
|
|
113
|
+
因此按前缀匹配、不做 glob 展开 —— 展开需要完整文件树,
|
|
114
|
+
而这里只需要回答"改了这个文件该找谁"。
|
|
115
|
+
|
|
116
|
+
唯一键是 **(章节, 模块)** 而不是模块名:模块名只在章节内唯一,
|
|
117
|
+
实测 ``smmu`` 在架构与驱动两个章节下各有一行,指的是两批人。
|
|
118
|
+
用模块名做键会让第二行撞约束,整份名单一起同步失败。
|
|
119
|
+
"""
|
|
120
|
+
|
|
121
|
+
__tablename__ = "ksp_subsystem_owner"
|
|
122
|
+
__table_args__ = (
|
|
123
|
+
UniqueConstraint("section", "module", name="uq_ksp_subsystem_owner_section_module"),
|
|
124
|
+
Index("ix_ksp_subsystem_owner_paths", "paths", postgresql_using="gin"),
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
|
128
|
+
|
|
129
|
+
module: Mapped[str] = mapped_column(String(64), nullable=False)
|
|
130
|
+
section: Mapped[str | None] = mapped_column(String(64), nullable=True)
|
|
131
|
+
"""committers.md 里的二级标题,如 ``kernel core``、``drivers``。"""
|
|
132
|
+
|
|
133
|
+
paths: Mapped[list[str] | None] = mapped_column(ARRAY(Text), nullable=True)
|
|
134
|
+
committer_logins: Mapped[list[str] | None] = mapped_column(ARRAY(Text), nullable=True)
|
|
135
|
+
"""上游写的评审人 handle(人写的展示名)。与 ``SIGMember.atomgit_id`` 未必字面相同。"""
|
|
136
|
+
|
|
137
|
+
sequence: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
|
|
138
|
+
source_url: Mapped[str | None] = mapped_column(String(512), nullable=True)
|
|
139
|
+
last_synced_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
|
|
140
|
+
|
|
141
|
+
def __repr__(self) -> str:
|
|
142
|
+
return f"<SubsystemOwner {self.section}/{self.module}>"
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
class ReleaseBranch(Base, TimestampMixin):
|
|
146
|
+
"""分支/版本登记表。
|
|
147
|
+
|
|
148
|
+
能从分支名推出来的(产品线、系列、SP 序号)由 ``domain.release`` 算,
|
|
149
|
+
这张表只存**推不出来的事实**:上游基线、生命周期、维护策略说明。
|
|
150
|
+
猜这些字段会造出一个看起来权威实际是编的值,所以留给人填。
|
|
151
|
+
|
|
152
|
+
**一个分支属于一个仓库**,这条是 sig-info.yaml 明写的
|
|
153
|
+
(``repo_branch`` 段里每条都带 ``repo``),只是早先写入时被丢掉了 ——
|
|
154
|
+
结果这张表成了全局表,纳管第二个仓库时,OLK-6.6 会出现在
|
|
155
|
+
src-openeuler/kernel 的分支页上。同名分支在两边都存在
|
|
156
|
+
(master、openEuler-24.03-LTS-SP4 都是),所以这一列不是可有可无的。
|
|
157
|
+
"""
|
|
158
|
+
|
|
159
|
+
__tablename__ = "ksp_release_branch"
|
|
160
|
+
__table_args__ = (
|
|
161
|
+
UniqueConstraint("repository_id", "name", name="uq_ksp_release_branch_repo_name"),
|
|
162
|
+
Index("ix_ksp_release_branch_keepers", "keepers", postgresql_using="gin"),
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
|
166
|
+
|
|
167
|
+
repository_id: Mapped[uuid.UUID | None] = mapped_column(
|
|
168
|
+
UUID(as_uuid=True), ForeignKey("ksp_repository.id", ondelete="CASCADE"), nullable=True
|
|
169
|
+
)
|
|
170
|
+
"""所属仓库。为空表示上游还没说明它属于哪个仓库(或那个仓库尚未纳管)。"""
|
|
171
|
+
|
|
172
|
+
name: Mapped[str] = mapped_column(String(64), nullable=False)
|
|
173
|
+
"""规范化后的分支名,与 ``domain.release.normalize_branch_name`` 一致。"""
|
|
174
|
+
|
|
175
|
+
aliases: Mapped[list[str] | None] = mapped_column(ARRAY(Text), nullable=True)
|
|
176
|
+
"""上游出现过的其他写法(如 ``openEuler-24.03-LTS_SP1``)。
|
|
177
|
+
库里的 ``target_branch`` 存的是原始名,靠这个列表才能把两边对上。"""
|
|
178
|
+
|
|
179
|
+
kind: Mapped[str] = mapped_column(String(16), nullable=False, default="other")
|
|
180
|
+
series: Mapped[str | None] = mapped_column(String(32), nullable=True)
|
|
181
|
+
sp: Mapped[int | None] = mapped_column(Integer, nullable=True)
|
|
182
|
+
upstream_base: Mapped[str | None] = mapped_column(String(16), nullable=True)
|
|
183
|
+
|
|
184
|
+
state: Mapped[str] = mapped_column(String(16), nullable=False, default="active")
|
|
185
|
+
"""active / maintenance / eol。"""
|
|
186
|
+
|
|
187
|
+
note: Mapped[str | None] = mapped_column(Text, nullable=True)
|
|
188
|
+
"""维护策略说明。来自 sig/Kernel/README.md 的人工摘要。"""
|
|
189
|
+
|
|
190
|
+
keepers: Mapped[list[str] | None] = mapped_column(ARRAY(Text), nullable=True)
|
|
191
|
+
"""分支负责人账号,来自 sig-info.yaml 的 branches 段。"""
|
|
192
|
+
|
|
193
|
+
def __repr__(self) -> str:
|
|
194
|
+
return f"<ReleaseBranch {self.name} {self.state}>"
|