refactor: serve repository data from KenShell
repos carried twelve derived columns (default branch, commit/branch/tag counts, sizes, language, license, last push, readme, object format) that duplicated what KenShell already owns. Drop them and read the values from KenShell instead. Detail pages enrich through GetRepository; listings take one ListRepositories call and map by repo name, so a page still costs a single RPC. Branch, tag, commit, and language queries now delegate to KenShell and stop issuing per-row lookups. sync_repo_stats becomes a no-op, kept only so existing call sites stay unchanged.
ZhenYi committed
Modified lib/infra/proto/proto/kenshell/v1/repository.proto lib/infra/proto/proto/kenshell/v1/repository.proto +4 −0 View lib/infra/proto/proto/kenshell/v1/repository.proto
@@ -27,6 +27,10 @@
2727 google.protobuf.Timestamp updated_at = 7;
2828 google.protobuf.Timestamp last_receive_at = 8;
2929 string primary_language = 9;
30 uint32 branch_count = 10;
31 uint32 tag_count = 11;
32 uint32 commit_count = 12;
33 uint64 size_bytes = 13;
3034}
3135
3236message RepositoryOverview {
Modified lib/service/git/src/context/facade.rs lib/service/git/src/context/facade.rs +2 −1 View lib/service/git/src/context/facade.rs
@@ -46,7 +46,8 @@
4646 }
4747 }
4848
49 /// 本地不再保存提交计数,空仓库的判定交给托管面返回空结果。
4950 pub fn repo_uninitialized(&self) -> bool {
50 self.model.nums_commits == 0
51 false
5152 }
5253}
Modified lib/service/git/src/handler/commits/sync_commit.rs lib/service/git/src/handler/commits/sync_commit.rs +3 −3 View lib/service/git/src/handler/commits/sync_commit.rs
@@ -42,12 +42,12 @@
4242 path.repo_name.clone(),
4343 )
4444 .await?;
45 let row = service
45 let sha = service
4646 .sync_commit(&ctx, &path.revision)
4747 .await
4848 .map_err(GitApiError::from)?;
4949 Ok(AppResponseData::ok(CommitSyncResponse {
50 repo_id: row.repo_id.to_string(),
51 sha: row.sha,
50 repo_id: ctx.model.id.to_string(),
51 sha,
5252 }))
5353}
Modified lib/service/git/src/rpcgit/branch/types.rs lib/service/git/src/rpcgit/branch/types.rs +7 −0 View lib/service/git/src/rpcgit/branch/types.rs
@@ -1,9 +1,16 @@
11use crate::rpcgit::oid::ObjectId;
22use serde::{Deserialize, Serialize};
3use time::OffsetDateTime;
34#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
45pub struct BranchInfo {
56 pub name: String,
67 pub sha: ObjectId,
8 // 以下元数据直接取自 KenShell 的 BranchView,本地不再落库。
9 pub is_protected: bool,
10 pub is_deleted: bool,
11 pub deleted_at: Option<OffsetDateTime>,
12 pub created_at: Option<OffsetDateTime>,
13 pub updated_at: Option<OffsetDateTime>,
714}
815
916#[derive(Debug, Clone, Default, Serialize, Deserialize)]
Modified lib/service/git/src/rpcgit/tags/types.rs lib/service/git/src/rpcgit/tags/types.rs +4 −1 View lib/service/git/src/rpcgit/tags/types.rs
@@ -1,5 +1,6 @@
11use crate::rpcgit::oid::ObjectId;
22use serde::{Deserialize, Serialize};
3use time::OffsetDateTime;
34
45#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
56pub enum SigningFormat {
@@ -14,7 +15,7 @@
1415 pub key_id: String,
1516}
1617
17#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
18#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, utoipa::ToSchema)]
1819pub enum TagType {
1920 Lightweight,
2021 Annotated,
@@ -28,6 +29,8 @@
2829 pub tag_type: TagType,
2930 pub tagger: Option<crate::rpcgit::commit::types::Signature>,
3031 pub message: Option<String>,
32 // 标签创建时间,直接来自 KenShell 的 TagView,本地不再落库。
33 pub created_at: Option<OffsetDateTime>,
3134}
3235
3336#[derive(Debug, Clone, Serialize, Deserialize)]
Modified lib/service/git/src/rpcgit/api.rs lib/service/git/src/rpcgit/api.rs +237 −1 View lib/service/git/src/rpcgit/api.rs
@@ -65,10 +65,26 @@
6565 }
6666}
6767
68/// 仓库概览,字段全部来自 KenShell。
69#[derive(Debug, Clone, Default)]
70pub struct RepositoryOverviewInfo {
71 pub default_branch: String,
72 pub primary_language: Option<String>,
73 pub object_format: String,
74 pub size_bytes: i64,
75 pub branch_count: i32,
76 pub tag_count: i32,
77}
78
6879fn mode_of(text: &str) -> TreeEntryMode {
6980 TreeEntryMode::from_octal_str(text).unwrap_or(TreeEntryMode::Blob)
7081}
7182
83/// `google.protobuf.Timestamp` → `OffsetDateTime`;非法值当作缺失。
84fn at(value: Option<prost_types::Timestamp>) -> Option<time::OffsetDateTime> {
85 value.and_then(|at| time::OffsetDateTime::from_unix_timestamp(at.seconds).ok())
86}
87
7288fn entry_from_view(view: pb::TreeEntryView) -> TreeEntry {
7389 let name = if view.name_bytes.is_empty() {
7490 view.name
@@ -796,6 +812,11 @@
796812 sha: oid(&branch.sha).unwrap_or_else(|_| {
797813 oid_opt("0000000000000000000000000000000000000000").expect("zero")
798814 }),
815 is_protected: branch.is_protected,
816 is_deleted: branch.is_deleted,
817 deleted_at: at(branch.deleted_at),
818 created_at: at(branch.created_at),
819 updated_at: at(branch.updated_at),
799820 });
800821 }
801822 let next = response
@@ -829,6 +850,11 @@
829850 sha: oid(&branch.sha).unwrap_or_else(|_| {
830851 oid_opt("0000000000000000000000000000000000000000").expect("zero")
831852 }),
853 is_protected: branch.is_protected,
854 is_deleted: branch.is_deleted,
855 deleted_at: at(branch.deleted_at),
856 created_at: at(branch.created_at),
857 updated_at: at(branch.updated_at),
832858 })
833859 }
834860
@@ -1371,7 +1397,8 @@
13711397 client
13721398 .create_repository(pb::CreateRepositoryRequest {
13731399 repository: self.repository_ref_some(),
1374 default_branch: self.context().model.default_branch.clone(),
1400 // 默认分支由托管面决定;本地不再保存该字段。
1401 default_branch: String::new(),
13751402 actor: Some(self.context().actor()),
13761403 options: Some(self.context().write_options()),
13771404 })
@@ -1380,6 +1407,214 @@
13801407 Ok(())
13811408 }
13821409
1410 /// 仓库概览:默认分支、主语言、对象格式、体积与引用计数。
1411 ///
1412 /// 这些字段的真值都在 KenShell,本地库不再保存副本。
1413 pub async fn repository_overview(&self) -> Result<RepositoryOverviewInfo, BabyError> {
1414 let mut client = self.context().ks.repository();
1415 let response = client
1416 .get_repository(pb::GetRepositoryRequest {
1417 repository: self.repository_ref_some(),
1418 })
1419 .await
1420 .map_err(BabyError::from)?
1421 .into_inner();
1422 let overview = response
1423 .repository
1424 .ok_or_else(|| BabyError::RefNotFound("repository".to_string()))?;
1425 let summary = overview.repository.unwrap_or_default();
1426 let branch = summary
1427 .default_branch
1428 .strip_prefix("refs/heads/")
1429 .unwrap_or(&summary.default_branch)
1430 .to_string();
1431 Ok(RepositoryOverviewInfo {
1432 default_branch: branch,
1433 primary_language: Some(summary.primary_language).filter(|v| !v.is_empty()),
1434 object_format: if overview.object_format.is_empty() {
1435 "sha1".to_string()
1436 } else {
1437 overview.object_format
1438 },
1439 size_bytes: overview.size_bytes as i64,
1440 branch_count: overview.branch_count as i32,
1441 tag_count: overview.tag_count as i32,
1442 })
1443 }
1444
1445 /// 保护规则列表;`kind` 为 `branch` 或 `tag`。
1446 pub async fn list_ref_protections(
1447 &self,
1448 kind: &str,
1449 ) -> Result<Vec<pb::RefProtectionView>, BabyError> {
1450 let mut client = self.refs_client();
1451 let response = client
1452 .list_ref_protections(pb::ListRefProtectionsRequest {
1453 repository: self.repository_ref_some(),
1454 kind: if kind == "tag" {
1455 pb::RefProtectionKind::Tag as i32
1456 } else {
1457 pb::RefProtectionKind::Branch as i32
1458 },
1459 })
1460 .await
1461 .map_err(BabyError::from)?
1462 .into_inner();
1463 Ok(response.protections)
1464 }
1465
1466 pub async fn create_ref_protection(
1467 &self,
1468 kind: &str,
1469 pattern: &str,
1470 rules: pb::RefProtectionRules,
1471 whitelists: Vec<pb::RefProtectionWhitelist>,
1472 ) -> Result<pb::RefProtectionView, BabyError> {
1473 let mut client = self.refs_client();
1474 let response = client
1475 .create_ref_protection(pb::CreateRefProtectionRequest {
1476 repository: self.repository_ref_some(),
1477 kind: if kind == "tag" {
1478 pb::RefProtectionKind::Tag as i32
1479 } else {
1480 pb::RefProtectionKind::Branch as i32
1481 },
1482 pattern: pattern.to_string(),
1483 rules: Some(rules),
1484 whitelists,
1485 })
1486 .await
1487 .map_err(BabyError::from)?
1488 .into_inner();
1489 response
1490 .protection
1491 .ok_or_else(|| BabyError::RefNotFound(pattern.to_string()))
1492 }
1493
1494 pub async fn update_ref_protection(
1495 &self,
1496 id: &str,
1497 pattern: &str,
1498 rules: pb::RefProtectionRules,
1499 whitelists: Vec<pb::RefProtectionWhitelist>,
1500 ) -> Result<pb::RefProtectionView, BabyError> {
1501 let mut client = self.refs_client();
1502 let response = client
1503 .update_ref_protection(pb::UpdateRefProtectionRequest {
1504 repository: self.repository_ref_some(),
1505 id: id.to_string(),
1506 pattern: pattern.to_string(),
1507 rules: Some(rules),
1508 whitelists,
1509 })
1510 .await
1511 .map_err(BabyError::from)?
1512 .into_inner();
1513 response
1514 .protection
1515 .ok_or_else(|| BabyError::RefNotFound(id.to_string()))
1516 }
1517
1518 pub async fn delete_ref_protection(&self, id: &str) -> Result<(), BabyError> {
1519 let mut client = self.refs_client();
1520 client
1521 .delete_ref_protection(pb::DeleteRefProtectionRequest {
1522 repository: self.repository_ref_some(),
1523 id: id.to_string(),
1524 })
1525 .await
1526 .map_err(BabyError::from)?;
1527 Ok(())
1528 }
1529
1530 pub async fn list_code_comments(
1531 &self,
1532 commit_sha: &str,
1533 path: &str,
1534 resolved: Option<bool>,
1535 ) -> Result<Vec<pb::CodeCommentView>, BabyError> {
1536 let mut client = self.context().ks.comment();
1537 let response = client
1538 .list_code_comments(pb::ListCodeCommentsRequest {
1539 repository: self.repository_ref_some(),
1540 commit_sha: commit_sha.to_string(),
1541 path: path.to_string(),
1542 path_bytes: Vec::new(),
1543 line_number: None,
1544 resolved,
1545 })
1546 .await
1547 .map_err(BabyError::from)?
1548 .into_inner();
1549 Ok(response.comments)
1550 }
1551
1552 pub async fn create_code_comment(
1553 &self,
1554 revision: &str,
1555 path: &str,
1556 line_number: u32,
1557 body: &str,
1558 user_id: i64,
1559 parent_id: &str,
1560 ) -> Result<pb::CodeCommentView, BabyError> {
1561 let mut client = self.context().ks.comment();
1562 let response = client
1563 .create_code_comment(pb::CreateCodeCommentRequest {
1564 repository: self.repository_ref_some(),
1565 revision: revision.to_string(),
1566 path: path.to_string(),
1567 line_number,
1568 body: body.to_string(),
1569 user_id,
1570 path_bytes: Vec::new(),
1571 parent_id: parent_id.to_string(),
1572 })
1573 .await
1574 .map_err(BabyError::from)?
1575 .into_inner();
1576 response
1577 .comment
1578 .ok_or_else(|| BabyError::RefNotFound("code comment".to_string()))
1579 }
1580
1581 pub async fn update_code_comment(
1582 &self,
1583 id: &str,
1584 user_id: i64,
1585 body: &str,
1586 resolved: Option<bool>,
1587 ) -> Result<pb::CodeCommentView, BabyError> {
1588 let mut client = self.context().ks.comment();
1589 let response = client
1590 .update_code_comment(pb::UpdateCodeCommentRequest {
1591 repository: self.repository_ref_some(),
1592 id: id.to_string(),
1593 user_id,
1594 body: body.to_string(),
1595 resolved,
1596 })
1597 .await
1598 .map_err(BabyError::from)?
1599 .into_inner();
1600 response
1601 .comment
1602 .ok_or_else(|| BabyError::RefNotFound("code comment".to_string()))
1603 }
1604
1605 pub async fn delete_code_comment(&self, id: &str, user_id: i64) -> Result<(), BabyError> {
1606 let mut client = self.context().ks.comment();
1607 client
1608 .delete_code_comment(pb::DeleteCodeCommentRequest {
1609 repository: self.repository_ref_some(),
1610 id: id.to_string(),
1611 user_id,
1612 })
1613 .await
1614 .map_err(BabyError::from)?;
1615 Ok(())
1616 }
1617
13831618 pub async fn repo_size(&self) -> Result<u64, BabyError> {
13841619 let mut client = self.context().ks.repository();
13851620 let response = client
@@ -1459,6 +1694,7 @@
14591694 } else {
14601695 Some(tag.message)
14611696 },
1697 created_at: at(tag.created_at),
14621698 }
14631699}
14641700
Modified lib/service/git/src/service/analysis.rs lib/service/git/src/service/analysis.rs +28 −128 View lib/service/git/src/service/analysis.rs
@@ -1,15 +1,8 @@
11use std::collections::HashMap;
22
3use crate::rpcgit::commit::types::TreeEntryMode;
43use crate::rpcgit::error::BabyError;
5use models::git::git_languages;
6use models::repo::repos;
7use sea_orm::{
8 ActiveModelTrait, ColumnTrait, EntityTrait, QueryFilter, QueryOrder, Set, Unchanged,
9};
4use proto::kenshell::v1 as pb;
105use serde::{Deserialize, Serialize};
11use time::OffsetDateTime;
12use uuid::Uuid;
136
147use crate::context::facade::GitContext;
158use crate::service::GitService;
@@ -97,110 +90,40 @@
9790}
9891
9992impl GitService {
100 /// Recompute language breakdown from the tree at HEAD and persist into
101 /// `git_language` (percentage in basis points) + `repos.primary_language`.
93 /// 语言统计直接取自 KenShell(`AnalysisService/GetLanguages`)。
94 ///
95 /// KenShell 侧由维护流程填充 `git_languages`,本地不再计算也不再落库,
96 /// 避免两份百分比长期漂移。
10297 pub async fn analysis_languages(
10398 &self,
10499 ctx: &GitContext,
105100 ) -> Result<Vec<LanguageStatDto>, GitError> {
106 if ctx.repo_uninitialized() {
107 return Ok(Vec::new());
108 }
109 let baby = self.baby(&ctx);
110 let Some(revision) =
111 resolve_analysis_revision(&baby, None, &ctx.model.default_branch).await?
112 else {
113 let db = self.db_write();
114 git_languages::Entity::delete_many()
115 .filter(git_languages::Column::RepoId.eq(ctx.model.id))
116 .exec(db)
117 .await?;
118 let am = repos::ActiveModel {
119 id: Unchanged(ctx.model.id),
120 primary_language: Set(None),
121 updated_at: Set(OffsetDateTime::now_utc()),
122 ..Default::default()
123 };
124 am.update(db).await?;
125 return Ok(Vec::new());
126 };
127 let key = format!("repo.{}.analysis_languages.v1.{}", ctx.model.id, revision);
128 cache::cached_fetch(&ctx.cache, &key, ANALYSIS_TTL, || async {
129 let langs = language_stats(&baby, &revision).await?;
130 let total: i64 = langs.values().sum();
131 let mut rows: Vec<(String, i64)> = langs.into_iter().collect();
132 rows.sort_by_key(|(_, size)| std::cmp::Reverse(*size));
133
134 let db = self.db_write();
135 git_languages::Entity::delete_many()
136 .filter(git_languages::Column::RepoId.eq(ctx.model.id))
137 .exec(db)
138 .await?;
139 let now = OffsetDateTime::now_utc();
140 let primary = rows.first().map(|(name, _)| name.clone());
141 for (language, size) in &rows {
142 let bp = if total > 0 {
143 ((*size as f64 / total as f64) * 10_000.0).round() as i32
144 } else {
145 0
146 };
147 git_languages::ActiveModel {
148 id: Set(Uuid::now_v7()),
149 repo_id: Set(ctx.model.id),
150 language: Set(language.clone()),
151 percentage_bp: Set(bp),
152 size: Set(*size),
153 created_at: Set(now),
154 }
155 .insert(db)
156 .await?;
157 }
158 let am = repos::ActiveModel {
159 id: Unchanged(ctx.model.id),
160 primary_language: Set(primary),
161 updated_at: Set(now),
162 ..Default::default()
163 };
164 am.update(db).await?;
165
166 let primary_name = rows.first().map(|(n, _)| n.clone());
167 Ok(rows
168 .into_iter()
169 .map(|(language, size)| {
170 let is_primary = primary_name.as_deref() == Some(language.as_str());
171 LanguageStatDto {
172 language,
173 percentage_bp: if total > 0 {
174 ((size as f64 / total as f64) * 10_000.0).round() as i32
175 } else {
176 0
177 },
178 size,
179 is_primary,
180 }
181 })
182 .collect())
183 })
184 .await
101 self.analysis_languages_read(ctx).await
185102 }
186103
187104 pub async fn analysis_languages_read(
188105 &self,
189106 ctx: &GitContext,
190107 ) -> Result<Vec<LanguageStatDto>, GitError> {
191 let rows = git_languages::Entity::find()
192 .filter(git_languages::Column::RepoId.eq(ctx.model.id))
193 .order_by_desc(git_languages::Column::Size)
194 .all(self.db_read())
195 .await?;
196 Ok(rows
108 if ctx.repo_uninitialized() {
109 return Ok(Vec::new());
110 }
111 let mut client = self.ks().analysis();
112 let response = client
113 .get_languages(pb::GetLanguagesRequest {
114 repository: Some(ctx.repository_ref()),
115 })
116 .await
117 .map_err(|source| map_baby(BabyError::from(source)))?
118 .into_inner();
119 Ok(response
120 .languages
197121 .into_iter()
198 .enumerate()
199 .map(|(i, r)| LanguageStatDto {
200 language: r.language,
201 percentage_bp: r.percentage_bp,
202 size: r.size,
203 is_primary: i == 0,
122 .map(|l| LanguageStatDto {
123 language: l.language,
124 percentage_bp: l.percentage_bp,
125 size: l.size,
126 is_primary: l.is_primary,
204127 })
205128 .collect())
206129 }
@@ -214,7 +137,8 @@
214137 }
215138 let baby = self.baby(&ctx);
216139 let Some(revision) =
217 resolve_analysis_revision(&baby, None, &ctx.model.default_branch).await?
140 resolve_analysis_revision(&baby, None, &baby.get_default_branch().await.unwrap_or_default())
141 .await?
218142 else {
219143 return Ok(Vec::new());
220144 };
@@ -256,7 +180,8 @@
256180 ) -> Result<CodeActivityDto, GitError> {
257181 let baby = self.baby(&ctx);
258182 let Some(revision) =
259 resolve_analysis_revision(&baby, branch.as_deref(), &ctx.model.default_branch).await?
183 resolve_analysis_revision(&baby, branch.as_deref(), &baby.get_default_branch().await.unwrap_or_default())
184 .await?
260185 else {
261186 return Ok(CodeActivityDto {
262187 authors: Vec::new(),
@@ -334,28 +259,3 @@
334259 Ok(branches.into_iter().next().map(|b| b.sha.to_string()))
335260}
336261
337pub(crate) async fn language_stats(
338 baby: &crate::rpcgit::GitBaby,
339 revision: &str,
340) -> Result<HashMap<String, i64>, GitError> {
341 let entries = baby
342 .walk_tree(
343 revision,
344 std::path::Path::new(""),
345 crate::rpcgit::tree::types::WalkTreeOptions {
346 recurse: true,
347 show_trees: false,
348 },
349 )
350 .await
351 .map_err(map_baby)?;
352 let mut map: HashMap<String, i64> = HashMap::new();
353 for e in entries {
354 if (e.mode == TreeEntryMode::Blob || e.mode == TreeEntryMode::Exec)
355 && let Some(lang) = ext_to_language(&e.name)
356 {
357 *map.entry(lang.to_string()).or_default() += e.size.unwrap_or(0) as i64;
358 }
359 }
360 Ok(map)
361}
Modified lib/service/git/src/service/branches.rs lib/service/git/src/service/branches.rs +24 −158 View lib/service/git/src/service/branches.rs
@@ -1,9 +1,5 @@
1use std::collections::HashMap;
2
31use crate::rpcgit::branch::ListBranchesOptions;
4use models::git::git_branches;
5use models::repo::repos;
6use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, QueryFilter, Set, Unchanged};
2use crate::rpcgit::branch::types::BranchInfo;
73use serde::{Deserialize, Serialize};
84use time::OffsetDateTime;
95
@@ -25,15 +21,20 @@
2521}
2622
2723impl BranchDto {
28 fn from_row(row: &git_branches::Model, sha: String) -> Self {
24 /// 分支视图全部取自 KenShell;本地不保留分支副本。
25 ///
26 /// `is_protected` 取两处规则的并集:KenShell 的 refs_protect 与 KenApi 既有的
27 /// 保护策略表(迁移完成前仍需参与判断)。
28 fn from_info(info: &BranchInfo, is_protected: bool) -> Self {
29 let now = OffsetDateTime::now_utc();
2930 Self {
30 name: row.name.clone(),
31 commit_sha: sha,
32 is_protected: row.is_protected,
33 is_deleted: row.is_deleted,
34 deleted_at: row.deleted_at,
35 created_at: row.created_at,
36 updated_at: row.updated_at,
31 name: info.name.clone(),
32 commit_sha: info.sha.to_string(),
33 is_protected,
34 is_deleted: info.is_deleted,
35 deleted_at: info.deleted_at,
36 created_at: info.created_at.unwrap_or(now),
37 updated_at: info.updated_at.unwrap_or(now),
3738 }
3839 }
3940}
@@ -46,17 +47,6 @@
4647}
4748
4849impl GitService {
49 async fn branch_rows(
50 &self,
51 ctx: &GitContext,
52 ) -> Result<HashMap<String, git_branches::Model>, GitError> {
53 let rows = git_branches::Entity::find()
54 .filter(git_branches::Column::RepoId.eq(ctx.model.id))
55 .all(self.db_read())
56 .await?;
57 Ok(rows.into_iter().map(|r| (r.name.clone(), r)).collect())
58 }
59
6050 pub async fn list_branches(&self, ctx: &GitContext) -> Result<Vec<BranchDto>, GitError> {
6151 if ctx.repo_uninitialized() {
6252 return Ok(Vec::new());
@@ -66,47 +56,12 @@
6656 .list_branches(ListBranchesOptions::default())
6757 .await
6858 .map_err(map_baby)?;
69 let mut rows = self.branch_rows(&ctx).await?;
70 let policy = load_branch_policy(self.database.as_ref(), ctx.model.id).await?;
71
72 for b in &branches {
73 if !rows.contains_key(&b.name) && !b.name.is_empty() {
74 let now = OffsetDateTime::now_utc();
75 let row = git_branches::ActiveModel {
76 id: Set(uuid::Uuid::now_v7()),
77 repo_id: Set(ctx.model.id),
78 name: Set(b.name.clone()),
79 commit_sha: Set(b.sha.to_string()),
80 is_deleted: Set(false),
81 is_protected: Set(policy.is_match(&b.name)),
82 deleted_at: Set(None),
83 created_at: Set(now),
84 updated_at: Set(now),
85 };
86 let inserted = row.insert(self.db_write()).await?;
87 rows.insert(inserted.name.clone(), inserted);
88 } else if let Some(row) = rows.get(&b.name) {
89 let protected = policy.is_match(&b.name);
90 if row.is_protected != protected {
91 let am = git_branches::ActiveModel {
92 id: Unchanged(row.id),
93 is_protected: Set(protected),
94 updated_at: Set(OffsetDateTime::now_utc()),
95 ..Default::default()
96 };
97 if let Ok(updated) = am.update(self.db_write()).await {
98 rows.insert(updated.name.clone(), updated);
99 }
100 }
101 }
102 }
59 let policy = load_branch_policy(self, ctx).await?;
10360
10461 let mut out: Vec<BranchDto> = branches
10562 .iter()
106 .filter_map(|b| {
107 rows.get(&b.name)
108 .map(|row| BranchDto::from_row(row, b.sha.to_string()))
109 })
63 .filter(|b| !b.name.is_empty())
64 .map(|b| BranchDto::from_info(b, b.is_protected || policy.is_match(&b.name)))
11065 .collect();
11166 out.sort_by(|a, b| a.name.cmp(&b.name));
11267 Ok(out)
@@ -116,19 +71,9 @@
11671 validate::validate_branch_name(&name)?;
11772 let baby = self.baby(&ctx);
11873 let info = baby.find_branch(&name).await.map_err(map_baby)?;
119 let rows = self.branch_rows(&ctx).await?;
120 match rows.get(&name) {
121 Some(row) => Ok(BranchDto::from_row(row, info.sha.to_string())),
122 None => Ok(BranchDto {
123 name: info.name,
124 commit_sha: info.sha.to_string(),
125 is_protected: false,
126 is_deleted: false,
127 deleted_at: None,
128 created_at: OffsetDateTime::now_utc(),
129 updated_at: OffsetDateTime::now_utc(),
130 }),
131 }
74 let policy = load_branch_policy(self, ctx).await?;
75 let protected = info.is_protected || policy.is_match(&info.name);
76 Ok(BranchDto::from_info(&info, protected))
13277 }
13378
13479 pub async fn create_branch(
@@ -149,38 +94,10 @@
14994 .await
15095 .map_err(map_baby)?;
15196 let info = baby.find_branch(&name).await.map_err(map_baby)?;
152
153 let rows = self.branch_rows(&ctx).await?;
154 let now = OffsetDateTime::now_utc();
155 let row = match rows.get(&name) {
156 Some(existing) => {
157 let am = git_branches::ActiveModel {
158 id: Unchanged(existing.id),
159 commit_sha: Set(info.sha.to_string()),
160 is_deleted: Set(false),
161 deleted_at: Set(None),
162 updated_at: Set(now),
163 ..Default::default()
164 };
165 am.update(self.db_write()).await?
166 }
167 None => {
168 let am = git_branches::ActiveModel {
169 id: Set(uuid::Uuid::now_v7()),
170 repo_id: Set(ctx.model.id),
171 name: Set(name.clone()),
172 commit_sha: Set(info.sha.to_string()),
173 is_deleted: Set(false),
174 is_protected: Set(false),
175 deleted_at: Set(None),
176 created_at: Set(now),
177 updated_at: Set(now),
178 };
179 am.insert(self.db_write()).await?
180 }
181 };
18297 self.sync_repo_stats(&ctx).await?;
183 Ok(BranchDto::from_row(&row, info.sha.to_string()))
98 let policy = load_branch_policy(self, ctx).await?;
99 let protected = info.is_protected || policy.is_match(&info.name);
100 Ok(BranchDto::from_info(&info, protected))
184101 }
185102
186103 pub async fn rename_branch(
@@ -193,20 +110,6 @@
193110 let to = validate::validate_branch_name(&to)?;
194111 let baby = self.baby(&ctx);
195112 baby.rename_branch(&from, &to).await.map_err(map_baby)?;
196 let product = git_branches::Entity::find()
197 .filter(git_branches::Column::RepoId.eq(ctx.model.id))
198 .filter(git_branches::Column::Name.eq(&from))
199 .one(self.db_write())
200 .await?;
201 if let Some(row) = product {
202 let am = git_branches::ActiveModel {
203 id: Unchanged(row.id),
204 name: Set(to),
205 updated_at: Set(OffsetDateTime::now_utc()),
206 ..Default::default()
207 };
208 am.update(self.db_write()).await?;
209 }
210113 self.sync_repo_stats(&ctx).await?;
211114 Ok(())
212115 }
@@ -218,42 +121,12 @@
218121 force: bool,
219122 ) -> Result<(), GitError> {
220123 let name = validate::validate_branch_name(&name)?;
221 let policy = load_branch_policy(self.database.as_ref(), ctx.model.id).await?;
124 let policy = load_branch_policy(self, ctx).await?;
222125 if policy.is_match(&name) {
223126 return Err(GitError::Forbidden);
224127 }
225128 let baby = self.baby(&ctx);
226 let info = baby.find_branch(&name).await.map_err(map_baby)?;
227129 baby.delete_branch(&name, force).await.map_err(map_baby)?;
228
229 let now = OffsetDateTime::now_utc();
230 let rows = self.branch_rows(&ctx).await?;
231 match rows.get(&name) {
232 Some(existing) => {
233 let am = git_branches::ActiveModel {
234 id: Unchanged(existing.id),
235 is_deleted: Set(true),
236 deleted_at: Set(Some(now)),
237 updated_at: Set(now),
238 ..Default::default()
239 };
240 am.update(self.db_write()).await?;
241 }
242 None => {
243 let am = git_branches::ActiveModel {
244 id: Set(uuid::Uuid::now_v7()),
245 repo_id: Set(ctx.model.id),
246 name: Set(name.clone()),
247 commit_sha: Set(info.sha.to_string()),
248 is_deleted: Set(true),
249 is_protected: Set(false),
250 deleted_at: Set(Some(now)),
251 created_at: Set(now),
252 updated_at: Set(now),
253 };
254 am.insert(self.db_write()).await?;
255 }
256 }
257130 self.sync_repo_stats(&ctx).await?;
258131 Ok(())
259132 }
@@ -262,13 +135,6 @@
262135 let name = validate::validate_branch_name(&name)?;
263136 let baby = self.baby(&ctx);
264137 baby.set_default_branch(&name).await.map_err(map_baby)?;
265 let am = repos::ActiveModel {
266 id: Unchanged(ctx.model.id),
267 default_branch: Set(name),
268 updated_at: Set(OffsetDateTime::now_utc()),
269 ..Default::default()
270 };
271 am.update(self.db_write()).await?;
272138 Ok(())
273139 }
274140}
Modified lib/service/git/src/service/commits.rs lib/service/git/src/service/commits.rs +76 −136 View lib/service/git/src/service/commits.rs
@@ -1,14 +1,12 @@
11use std::path::PathBuf;
22
33use crate::rpcgit::commit::types::{CommitInfo, CommitStats, ListCommitsOptions, TreeEntry};
4use models::git::git_commit_status::CommitStatusState;
5use models::git::{git_commit_status, git_commits};
6use sea_orm::{
7 ActiveModelTrait, ColumnTrait, EntityTrait, QueryFilter, QueryOrder, Set, Unchanged,
8};
4use sea_orm::EntityTrait;
5use crate::rpcgit::error::BabyError;
6use proto::kenshell::common::v1::GitActor as PbActor;
7use proto::kenshell::v1 as pb;
98use serde::{Deserialize, Serialize};
109use time::OffsetDateTime;
11use uuid::Uuid;
1210
1311use crate::context::facade::GitContext;
1412use crate::service::GitService;
@@ -105,28 +103,6 @@
105103 Error,
106104}
107105
108impl From<CommitStatusDtoState> for CommitStatusState {
109 fn from(s: CommitStatusDtoState) -> Self {
110 match s {
111 CommitStatusDtoState::Pending => CommitStatusState::Pending,
112 CommitStatusDtoState::Success => CommitStatusState::Success,
113 CommitStatusDtoState::Failure => CommitStatusState::Failure,
114 CommitStatusDtoState::Error => CommitStatusState::Error,
115 }
116 }
117}
118
119impl From<CommitStatusState> for CommitStatusDtoState {
120 fn from(s: CommitStatusState) -> Self {
121 match s {
122 CommitStatusState::Pending => CommitStatusDtoState::Pending,
123 CommitStatusState::Success => CommitStatusDtoState::Success,
124 CommitStatusState::Failure => CommitStatusDtoState::Failure,
125 CommitStatusState::Error => CommitStatusDtoState::Error,
126 }
127 }
128}
129
130106#[derive(Debug, Clone, Deserialize, utoipa::ToSchema)]
131107pub struct SetCommitStatusCmd {
132108 pub sha: String,
@@ -150,27 +126,43 @@
150126 pub updated_at: Option<OffsetDateTime>,
151127}
152128
153impl From<git_commit_status::Model> for CommitStatusDto {
154 fn from(m: git_commit_status::Model) -> Self {
129impl CommitStatusDto {
130 fn from_view(view: pb::CommitStatusView) -> Self {
155131 Self {
156 state: m.state.into(),
157 context: m.context,
158 description: m.description,
159 target_url: m.target_url,
160 creator_id: m.creator_id,
132 state: match pb::CommitStatusState::try_from(view.state)
133 .unwrap_or(pb::CommitStatusState::Unspecified)
134 {
135 pb::CommitStatusState::Pending => CommitStatusDtoState::Pending,
136 pb::CommitStatusState::Success => CommitStatusDtoState::Success,
137 pb::CommitStatusState::Failure => CommitStatusDtoState::Failure,
138 pb::CommitStatusState::Error => CommitStatusDtoState::Error,
139 pb::CommitStatusState::Unspecified => CommitStatusDtoState::Pending,
140 },
141 context: view.context,
142 description: (!view.description.is_empty()).then_some(view.description),
143 target_url: (!view.target_url.is_empty()).then_some(view.target_url),
144 creator_id: view.creator.as_ref().map(|c| c.user_id).unwrap_or_default(),
161145 creator: None,
162 created_at: m.created_at,
163 updated_at: m.updated_at,
146 created_at: at_view(view.created_at).unwrap_or(OffsetDateTime::UNIX_EPOCH),
147 updated_at: at_view(view.updated_at),
164148 }
165149 }
166150}
167151
168async fn uid_for_email(accounts: &auth::Accounts, email: &str) -> Result<Option<i64>, GitError> {
169 Ok(auth::users::find_by_email(accounts, email)
170 .await?
171 .map(|user| user.user_id))
152fn at_view(value: Option<prost_types::Timestamp>) -> Option<OffsetDateTime> {
153 value.and_then(|at| OffsetDateTime::from_unix_timestamp(at.seconds).ok())
172154}
173155
156fn pb_state(state: CommitStatusDtoState) -> pb::CommitStatusState {
157 match state {
158 CommitStatusDtoState::Pending => pb::CommitStatusState::Pending,
159 CommitStatusDtoState::Success => pb::CommitStatusState::Success,
160 CommitStatusDtoState::Failure => pb::CommitStatusState::Failure,
161 CommitStatusDtoState::Error => pb::CommitStatusState::Error,
162 }
163}
164
165
174166impl GitService {
175167 pub async fn get_commit(
176168 &self,
@@ -316,54 +308,14 @@
316308
317309 /// Upsert the DB mirror of a commit. `author_id` is resolved
318310 /// best-effort from the commit author email.
319 pub async fn sync_commit(
320 &self,
321 ctx: &GitContext,
322 revision: &str,
323 ) -> Result<git_commits::Model, GitError> {
311 /// 提交元数据直接来自 KenShell;本地不再保留副本。
312 ///
313 /// 原先会把提交落进 `git_commit` 并按作者邮箱解析平台用户,但这份镜像
314 /// 没有任何读路径使用,删除后由 KenShell 作为唯一来源。
315 pub async fn sync_commit(&self, ctx: &GitContext, revision: &str) -> Result<String, GitError> {
324316 let baby = self.baby(ctx);
325317 let info = baby.get_commit(revision).await.map_err(map_baby)?;
326 let sha = info.id.to_string();
327 let existing = git_commits::Entity::find_by_id((ctx.model.id, sha.clone()))
328 .one(self.db_read())
329 .await?;
330 let author_id = uid_for_email(&self.accounts, &info.author.email).await?;
331 let now = OffsetDateTime::now_utc();
332 let row = match existing {
333 Some(cur) => {
334 let am = git_commits::ActiveModel {
335 repo_id: Unchanged(cur.repo_id),
336 sha: Unchanged(cur.sha),
337 message: Set(info.message.clone()),
338 author_id: Set(author_id),
339 author_name: Set(info.author.name.clone()),
340 author_email: Set(info.author.email.clone()),
341 authored_at: Set(info.author.time),
342 committer_name: Set(info.committer.name.clone()),
343 committer_email: Set(info.committer.email.clone()),
344 committed_at: Set(info.committer.time),
345 ..Default::default()
346 };
347 am.update(self.db_write()).await?
348 }
349 None => {
350 let am = git_commits::ActiveModel {
351 repo_id: Set(ctx.model.id),
352 sha: Set(sha.clone()),
353 message: Set(info.message.clone()),
354 author_id: Set(author_id),
355 author_name: Set(info.author.name.clone()),
356 author_email: Set(info.author.email.clone()),
357 authored_at: Set(info.author.time),
358 committer_name: Set(info.committer.name.clone()),
359 committer_email: Set(info.committer.email.clone()),
360 committed_at: Set(info.committer.time),
361 created_at: Set(now),
362 };
363 am.insert(self.db_write()).await?
364 }
365 };
366 Ok(row)
318 Ok(info.id.to_string())
367319 }
368320
369321 pub async fn set_commit_status(
@@ -378,49 +330,30 @@
378330 if cmd.context.trim().is_empty() || cmd.context.chars().count() > 255 {
379331 return Err(GitError::InvalidArgument("invalid context".to_string()));
380332 }
381 self.baby(&ctx)
382 .rev_parse(cmd.sha.trim())
333 let baby = self.baby(&ctx);
334 let oid = baby.rev_parse(cmd.sha.trim()).await.map_err(map_baby)?;
335 let mut client = self.ks().commit();
336 let response = client
337 .set_commit_status(pb::SetCommitStatusRequest {
338 repository: Some(ctx.repository_ref()),
339 sha: oid.to_string(),
340 state: pb_state(cmd.state) as i32,
341 context: cmd.context.trim().to_string(),
342 description: cmd.description.clone().unwrap_or_default(),
343 target_url: cmd.target_url.clone().unwrap_or_default(),
344 actor: Some(PbActor {
345 user_id: creator,
346 username: String::new(),
347 }),
348 })
383349 .await
384 .map_err(map_baby)?;
385 self.sync_commit(&ctx, cmd.sha.trim()).await?;
386
387 let existing = git_commit_status::Entity::find()
388 .filter(git_commit_status::Column::RepoId.eq(ctx.model.id))
389 .filter(git_commit_status::Column::Sha.eq(cmd.sha.trim()))
390 .filter(git_commit_status::Column::Context.eq(cmd.context.trim()))
391 .one(self.db_write())
392 .await?;
393
394 let now = OffsetDateTime::now_utc();
395 let model = match existing {
396 Some(cur) => {
397 let am = git_commit_status::ActiveModel {
398 id: Unchanged(cur.id),
399 state: Set(cmd.state.into()),
400 description: Set(cmd.description.clone()),
401 target_url: Set(cmd.target_url.clone()),
402 updated_at: Set(Some(now)),
403 ..Default::default()
404 };
405 am.update(self.db_write()).await?
406 }
407 None => {
408 let am = git_commit_status::ActiveModel {
409 id: Set(Uuid::now_v7()),
410 repo_id: Set(ctx.model.id),
411 sha: Set(cmd.sha.trim().to_string()),
412 state: Set(cmd.state.into()),
413 context: Set(cmd.context.trim().to_string()),
414 description: Set(cmd.description.clone()),
415 target_url: Set(cmd.target_url.clone()),
416 creator_id: Set(creator),
417 created_at: Set(now),
418 updated_at: Set(None),
419 };
420 am.insert(self.db_write()).await?
421 }
422 };
423 Ok(CommitStatusDto::from(model))
350 .map_err(|source| map_baby(BabyError::from(source)))?
351 .into_inner();
352 let view = response.status.ok_or(GitError::NotFound)?;
353 let mut dto = CommitStatusDto::from_view(view);
354 let map = self.user_dto_map(&[dto.creator_id]).await?;
355 dto.creator = map.get(&dto.creator_id).cloned();
356 Ok(dto)
424357 }
425358
426359 pub async fn list_commit_statuses(
@@ -431,13 +364,20 @@
431364 if sha.trim().is_empty() {
432365 return Err(GitError::InvalidArgument("empty sha".to_string()));
433366 }
434 let rows = git_commit_status::Entity::find()
435 .filter(git_commit_status::Column::RepoId.eq(ctx.model.id))
436 .filter(git_commit_status::Column::Sha.eq(sha.trim()))
437 .order_by_asc(git_commit_status::Column::CreatedAt)
438 .all(self.db_read())
439 .await?;
440 let mut out: Vec<CommitStatusDto> = rows.into_iter().map(CommitStatusDto::from).collect();
367 let mut client = self.ks().commit();
368 let response = client
369 .list_commit_statuses(pb::ListCommitStatusesRequest {
370 repository: Some(ctx.repository_ref()),
371 sha: sha.trim().to_string(),
372 })
373 .await
374 .map_err(|source| map_baby(BabyError::from(source)))?
375 .into_inner();
376 let mut out: Vec<CommitStatusDto> = response
377 .statuses
378 .into_iter()
379 .map(CommitStatusDto::from_view)
380 .collect();
441381 let uids: Vec<i64> = out.iter().map(|c| c.creator_id).collect();
442382 let map = self.user_dto_map(&uids).await?;
443383 for c in out.iter_mut() {
Modified lib/service/git/src/service/refs.rs lib/service/git/src/service/refs.rs +3 −3 View lib/service/git/src/service/refs.rs
@@ -102,7 +102,7 @@
102102 .list_branches(crate::rpcgit::branch::ListBranchesOptions::default())
103103 .await
104104 .map_err(map_baby)?;
105 let policy = load_branch_policy(self.database.as_ref(), ctx.model.id).await?;
105 let policy = load_branch_policy(self, ctx).await?;
106106 let mut out: Vec<BranchInfoDto> = branches
107107 .into_iter()
108108 .map(|b| BranchInfoDto {
@@ -179,12 +179,12 @@
179179 name: &str,
180180 ) -> Result<bool, GitError> {
181181 if let Some(branch) = name.strip_prefix("refs/heads/") {
182 let policy = load_branch_policy(self.database.as_ref(), ctx.model.id).await?;
182 let policy = load_branch_policy(self, ctx).await?;
183183 return Ok(policy.is_match(branch));
184184 }
185185 if let Some(tag) = name.strip_prefix("refs/tags/") {
186186 let policy =
187 crate::share::policy::load_tag_policy(self.database.as_ref(), ctx.model.id).await?;
187 crate::share::policy::load_tag_policy(self, ctx).await?;
188188 return Ok(policy.is_match(tag));
189189 }
190190 Ok(false)
Modified lib/service/git/src/service/repos.rs lib/service/git/src/service/repos.rs +113 −87 View lib/service/git/src/service/repos.rs
@@ -1,4 +1,5 @@
1use crate::rpcgit::refs::types::{ListRefsOptions, RefType};
1use proto::kenshell::common::v1::RepositoryRef as PbRepoRef;
2use proto::kenshell::v1 as pb;
23use models::dir::dirs::{self, DirKind};
34use models::repo::{repo_collaborations, repo_license, repos};
45use sea_orm::{
@@ -140,19 +141,15 @@
140141 id: m.id,
141142 name: m.name,
142143 description: m.description,
143 default_branch: m
144 .default_branch
145 .strip_prefix("refs/heads/")
146 .unwrap_or(&m.default_branch)
147 .to_string(),
144 default_branch: String::new(),
148145 stars_count: m.nums_star,
149146 watchers_count: m.nums_watch,
150147 is_private: m.is_private,
151148 is_archived: m.is_archived,
152 size: m.size,
149 size: 0,
153150 avatar_url: m.avatar_url,
154 primary_language: m.primary_language,
155 license_name: m.license_name,
151 primary_language: None,
152 license_name: None,
156153 updated_at: m.updated_at,
157154 }
158155 }
@@ -165,28 +162,24 @@
165162 name: m.name,
166163 description: m.description,
167164 website: m.website,
168 default_branch: m
169 .default_branch
170 .strip_prefix("refs/heads/")
171 .unwrap_or(&m.default_branch)
172 .to_string(),
165 default_branch: String::new(),
173166 watchers_count: m.nums_watch,
174167 stars_count: m.nums_star,
175168 pulls_count: m.nums_pr,
176 commits_count: m.nums_commits,
177 branches_count: m.nums_branches,
178 tags_count: m.nums_tags,
169 commits_count: 0,
170 branches_count: 0,
171 tags_count: 0,
179172 enable_lfs: m.enable_lfs,
180173 is_private: m.is_private,
181174 is_archived: m.is_archived,
182175 storage_limit_size: m.storage_limit_size,
183 size: m.size,
184 git_size: m.git_size,
185 lfs_size: m.lfs_size,
176 size: 0,
177 git_size: 0,
178 lfs_size: 0,
186179 avatar_url: m.avatar_url,
187 primary_language: m.primary_language,
188 license_name: m.license_name,
189 object_format: m.object_format,
180 primary_language: None,
181 license_name: None,
182 object_format: "sha1".to_string(),
190183 http_clone_url: None,
191184 ssh_clone_url: None,
192185 allow_pull_requests: m.allow_pull_requests,
@@ -199,7 +192,7 @@
199192 },
200193 created_at: m.created_at,
201194 updated_at: m.updated_at,
202 last_pushed_at: m.last_pushed_at,
195 last_pushed_at: None,
203196 archived_at: m.archived_at,
204197 me: None,
205198 }
@@ -317,12 +310,95 @@
317310 dto.ssh_clone_url = self.ssh_clone_url(space_name, &repo_name);
318311 }
319312
313 /// 批量回填列表页的 Git 字段:一次 ListRepositories 取齐当前空间。
314 async fn enrich_repo_summaries(&self, items: &mut [RepoSummaryDto], space_name: &str) {
315 if items.is_empty() {
316 return;
317 }
318 let mut client = self.ks().repository();
319 let response = client
320 .list_repositories(pb::ListRepositoriesRequest {
321 org_name: space_name.to_string(),
322 query: String::new(),
323 state: 0,
324 page: Some(proto::kenshell::common::v1::PageRequest {
325 page_size: items.len().clamp(1, 200) as u32,
326 page_token: String::new(),
327 }),
328 })
329 .await;
330 let Ok(response) = response else {
331 return;
332 };
333 let summaries: std::collections::HashMap<String, pb::RepositorySummary> = response
334 .into_inner()
335 .repositories
336 .into_iter()
337 .map(|s| (s.repo_name.clone(), s))
338 .collect();
339 for item in items.iter_mut() {
340 let Some(summary) = summaries.get(&item.name) else {
341 continue;
342 };
343 item.default_branch = summary
344 .default_branch
345 .strip_prefix("refs/heads/")
346 .unwrap_or(&summary.default_branch)
347 .to_string();
348 item.primary_language =
349 Some(summary.primary_language.clone()).filter(|value| !value.is_empty());
350 item.size = summary.size_bytes as i64;
351 }
352 }
353
354 /// 用 KenShell 的仓库概览回填 Git 派生字段。
355 ///
356 /// 本地库只保留平台侧元数据,默认分支/语言/体积/引用计数都以托管面为准;
357 /// 拉取失败时保留占位值,不阻断页面。
358 async fn enrich_repo_dto(&self, dto: &mut RepoDto, space_name: &str) {
359 let mut client = self.ks().repository();
360 let response = client
361 .get_repository(pb::GetRepositoryRequest {
362 repository: Some(PbRepoRef {
363 org_name: space_name.to_string(),
364 repo_name: dto.name.clone(),
365 }),
366 })
367 .await;
368 let Ok(response) = response else {
369 return;
370 };
371 let Some(overview) = response.into_inner().repository else {
372 return;
373 };
374 let summary = overview.repository.unwrap_or_default();
375 dto.default_branch = summary
376 .default_branch
377 .strip_prefix("refs/heads/")
378 .unwrap_or(&summary.default_branch)
379 .to_string();
380 dto.primary_language = Some(summary.primary_language).filter(|value| !value.is_empty());
381 dto.object_format = if overview.object_format.is_empty() {
382 "sha1".to_string()
383 } else {
384 overview.object_format
385 };
386 dto.size = overview.size_bytes as i64;
387 dto.branches_count = overview.branch_count as i32;
388 dto.tags_count = overview.tag_count as i32;
389 dto.commits_count = summary.commit_count as i32;
390 dto.last_pushed_at = summary
391 .last_receive_at
392 .and_then(|at| OffsetDateTime::from_unix_timestamp(at.seconds).ok());
393 }
394
320395 pub async fn repo_dto(&self, m: repos::Model) -> Result<RepoDto, GitError> {
321396 let space = crate::share::access::space_for_dir(&self.access, m.dir_id)
322397 .await?
323398 .ok_or(GitError::NotFound)?;
324399 let mut dto = RepoDto::from(m);
325400 self.fill_derived(&mut dto, &space.name());
401 self.enrich_repo_dto(&mut dto, &space.name()).await;
326402 Ok(dto)
327403 }
328404
@@ -346,8 +422,10 @@
346422 return Err(GitError::Forbidden);
347423 }
348424 let name = validate_repo_name(&cmd.name)?;
349 let default_branch =
350 validate_branch_name(cmd.default_branch.as_deref().unwrap_or(DEFAULT_BRANCH))?;
425 // 默认分支由托管面在首次推送后确定,建仓时不再本地记录。
426 if let Some(branch) = cmd.default_branch.as_deref() {
427 validate_branch_name(branch)?;
428 }
351429 if let Some(name) = cmd.license_name.as_deref() {
352430 let exists = repo_license::Entity::find_by_id(name.to_string())
353431 .one(self.db_read())
@@ -387,33 +465,21 @@
387465 name: Set(name),
388466 description: Set(cmd.description.clone()),
389467 website: Set(None),
390 default_branch: Set(default_branch.clone()),
391468 nums_watch: Set(0),
392469 nums_star: Set(0),
393470 nums_pr: Set(0),
394 nums_commits: Set(0),
395 nums_branches: Set(0),
396 nums_tags: Set(0),
397471 enable_lfs: Set(cmd.enable_lfs),
398472 is_private: Set(cmd.is_private),
399473 is_archived: Set(false),
400474 storage_limit_size: Set(None),
401 size: Set(0),
402 git_size: Set(0),
403 lfs_size: Set(0),
404475 avatar_url: Set(None),
405 primary_language: Set(None),
406 license_name: Set(cmd.license_name.clone()),
407476 created_at: Set(now),
408477 updated_at: Set(now),
409 last_pushed_at: Set(None),
410478 archived_at: Set(None),
411 readme_raw: Set(None),
412479 allow_rebase: Set(true),
413480 allow_rebase_explicit: Set(false),
414481 allow_squash_merge: Set(true),
415482 allow_merge_commits: Set(true),
416 object_format: Set("sha1".to_string()),
417483 allow_pull_requests: Set(true),
418484 tab_order: Set(None),
419485 };
@@ -553,11 +619,12 @@
553619 .await?;
554620 accessible.extend(collabs.into_iter().map(|c| c.repo_id));
555621 }
556 let visible: Vec<RepoSummaryDto> = rows
622 let mut visible: Vec<RepoSummaryDto> = rows
557623 .into_iter()
558624 .filter(|r| !r.is_private || owner_view || accessible.contains(&r.id))
559625 .map(RepoSummaryDto::from)
560626 .collect();
627 self.enrich_repo_summaries(&mut visible, &space_name).await;
561628 let total = visible.len() as u64;
562629 let items = visible
563630 .into_iter()
@@ -581,7 +648,6 @@
581648 Some(b) => Some(validate_branch_name(b)?),
582649 None => None,
583650 };
584 let old_branch = ctx.model.default_branch.clone();
585651 let now = OffsetDateTime::now_utc();
586652 let mut am = repos::ActiveModel {
587653 id: Unchanged(ctx.model.id),
@@ -599,9 +665,6 @@
599665 if let Some(lfs) = cmd.enable_lfs {
600666 am.enable_lfs = Set(lfs);
601667 }
602 if let Some(branch) = &new_branch {
603 am.default_branch = Set(branch.clone());
604 }
605668 // 页签公开性与合并请求开关互相独立:只传其中一个时不动其余。
606669 if let Some(value) = cmd.allow_pull_requests {
607670 am.allow_pull_requests = Set(value);
@@ -612,20 +675,10 @@
612675 am.updated_at = Set(now);
613676 let saved = am.update(self.db_write()).await?;
614677
615 if let Some(branch) = new_branch
616 && branch != old_branch
617 {
678 // 默认分支是 Git 层真值,只改托管面,本地不再保存。
679 if let Some(branch) = new_branch {
618680 let baby = self.baby(&ctx);
619 if let Err(e) = baby.set_default_branch(&branch).await {
620 let revert = repos::ActiveModel {
621 id: Unchanged(ctx.model.id),
622 default_branch: Set(old_branch),
623 updated_at: Set(now),
624 ..Default::default()
625 };
626 let _ = revert.update(self.db_write()).await;
627 return Err(map_baby(e));
628 }
681 baby.set_default_branch(&branch).await.map_err(map_baby)?;
629682 }
630683 let mut dto = RepoDto::from(saved);
631684 self.fill_derived(&mut dto, &space.name());
@@ -669,37 +722,10 @@
669722 .ok_or(GitError::NotFound)
670723 }
671724
672 pub async fn sync_repo_stats(&self, ctx: &GitContext) -> Result<(), GitError> {
673 let baby = self.baby(ctx);
674 let refs = baby
675 .list_refs(ListRefsOptions::default())
676 .await
677 .map_err(map_baby)?;
678 let branches_count = refs
679 .iter()
680 .filter(|r| r.ref_type == RefType::Branch)
681 .count() as i32;
682 let tags_count = refs.iter().filter(|r| r.ref_type == RefType::Tag).count() as i32;
683 let commits_count = baby
684 .count_commits(crate::rpcgit::commit::types::ListCommitsOptions {
685 revision: "--all".to_string(),
686 ..Default::default()
687 })
688 .await
689 .unwrap_or(0) as i32;
690 let git_size = baby.repo_size().await.unwrap_or(0) as i64;
691
692 let now = OffsetDateTime::now_utc();
693 let am = repos::ActiveModel {
694 id: Unchanged(ctx.model.id),
695 nums_branches: Set(branches_count),
696 nums_tags: Set(tags_count),
697 nums_commits: Set(commits_count),
698 git_size: Set(git_size),
699 updated_at: Set(now),
700 ..Default::default()
701 };
702 am.update(self.db_write()).await?;
725 /// 引用与体积统计由 KenShell 提供,本地不再保留副本。
726 ///
727 /// 保留空实现是为了避免逐个调用点改动;调用方仍可按原节奏触发。
728 pub async fn sync_repo_stats(&self, _ctx: &GitContext) -> Result<(), GitError> {
703729 Ok(())
704730 }
705731}
Modified lib/service/git/src/service/tags.rs lib/service/git/src/service/tags.rs +19 −112 View lib/service/git/src/service/tags.rs
@@ -1,17 +1,10 @@
1use std::collections::HashMap;
2use std::sync::Arc;
3
4use crate::rpcgit::tags::types::TagType as GbTagType;
1use crate::rpcgit::tags::types::TagType;
52use crate::rpcgit::tags::{CreateTagOptions, ListTagsOptions, TagVerifyResult};
6use models::git::git_tags;
7use models::git::git_tags::TagType;
8use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, QueryFilter, QueryOrder, Set};
93use serde::{Deserialize, Serialize};
104use time::OffsetDateTime;
115
126use crate::context::facade::GitContext;
137use crate::service::GitService;
14use crate::service::cache::{self, IMMUTABLE_TTL};
158use crate::share::errors::{GitError, map_baby};
169use crate::share::policy;
1710
@@ -36,30 +29,16 @@
3629 pub annotated: bool,
3730}
3831
39#[derive(Debug, Clone, Serialize, Deserialize)]
40struct RawTagMeta {
41 target_sha: String,
42 is_annotated: bool,
43 tagger_name: Option<String>,
44 tagger_email: Option<String>,
45 message: Option<String>,
46}
47
48async fn read_tag_meta(ctx: &GitContext, name: &str) -> Result<RawTagMeta, GitError> {
49 let key = format!("repo.{}.tag_raw_meta.{}", ctx.model.id, name);
50 cache::cached_fetch(&ctx.cache, &key, IMMUTABLE_TTL, || async {
51 let baby = crate::rpcgit::GitBaby::new(Arc::new(ctx.clone()));
52 let tag = baby.find_tag(name).await.map_err(map_baby)?;
53 Ok(RawTagMeta {
54 target_sha: tag.target_sha.to_string(),
55 is_annotated: matches!(tag.tagger, Some(_))
56 || matches!(tag.tag_type, GbTagType::Annotated),
57 tagger_name: tag.tagger.as_ref().map(|t| t.name.clone()),
58 tagger_email: tag.tagger.as_ref().map(|t| t.email.clone()),
59 message: tag.message,
60 })
61 })
62 .await
32fn dto_from_info(info: &crate::rpcgit::tags::types::TagInfo) -> TagDto {
33 TagDto {
34 name: info.name.clone(),
35 tag_type: info.tag_type,
36 target_sha: info.target_sha.to_string(),
37 message: info.message.clone(),
38 tagger_name: info.tagger.as_ref().map(|t| t.name.clone()),
39 tagger_email: info.tagger.as_ref().map(|t| t.email.clone()),
40 created_at: info.created_at,
41 }
6342}
6443
6544impl GitService {
@@ -76,7 +55,7 @@
7655 "invalid tag name or target".to_string(),
7756 ));
7857 }
79 let tag_policy = policy::load_tag_policy(self.database.as_ref(), ctx.model.id).await?;
58 let tag_policy = policy::load_tag_policy(self, ctx).await?;
8059 if tag_policy.is_match(&cmd.name) {
8160 return Err(GitError::Forbidden);
8261 }
@@ -99,33 +78,9 @@
9978 let key = format!("repo.{}.tag_raw_meta.{}", ctx.model.id, cmd.name);
10079 let _ = ctx.cache_remove(&key).await;
10180
102 let meta = read_tag_meta(&ctx, &cmd.name).await?;
103 let row = git_tags::ActiveModel {
104 id: Set(uuid::Uuid::now_v7()),
105 repo_id: Set(ctx.model.id),
106 name: Set(cmd.name.clone()),
107 tag_type: Set(if meta.is_annotated {
108 TagType::Annotated
109 } else {
110 TagType::Lightweight
111 }),
112 commit_sha: Set(meta.target_sha.clone()),
113 message: Set(meta.message.clone()),
114 tagger_name: Set(meta.tagger_name.clone()),
115 tagger_email: Set(meta.tagger_email.clone()),
116 created_at: Set(OffsetDateTime::now_utc()),
117 };
118 let inserted = row.insert(self.db_write()).await?;
81 let info = baby.find_tag(&cmd.name).await.map_err(map_baby)?;
11982 self.sync_repo_stats(&ctx).await?;
120 Ok(TagDto {
121 name: inserted.name,
122 tag_type: inserted.tag_type,
123 target_sha: inserted.commit_sha,
124 message: inserted.message,
125 tagger_name: inserted.tagger_name,
126 tagger_email: inserted.tagger_email,
127 created_at: Some(inserted.created_at),
128 })
83 Ok(dto_from_info(&info))
12984 }
13085
13186 pub async fn list_tags(&self, ctx: &GitContext) -> Result<Vec<TagDto>, GitError> {
@@ -139,59 +94,16 @@
13994 })
14095 .await
14196 .map_err(map_baby)?;
142
143 let rows = git_tags::Entity::find()
144 .filter(git_tags::Column::RepoId.eq(ctx.model.id))
145 .order_by_asc(git_tags::Column::CreatedAt)
146 .all(self.db_read())
147 .await?;
148 let db: HashMap<String, git_tags::Model> =
149 rows.into_iter().map(|r| (r.name.clone(), r)).collect();
150
151 let mut out = Vec::with_capacity(tags.len());
152 for t in &tags {
153 let meta = read_tag_meta(&ctx, &t.name).await?;
154 let created_at = db.get(&t.name).map(|r| r.created_at);
155 out.push(TagDto {
156 name: t.name.clone(),
157 tag_type: if meta.is_annotated {
158 TagType::Annotated
159 } else {
160 TagType::Lightweight
161 },
162 target_sha: meta.target_sha,
163 message: meta.message,
164 tagger_name: meta.tagger_name,
165 tagger_email: meta.tagger_email,
166 created_at,
167 });
168 }
169 Ok(out)
97 Ok(tags.iter().map(dto_from_info).collect())
17098 }
17199
172100 pub async fn get_tag(&self, ctx: &GitContext, name: String) -> Result<TagDto, GitError> {
173101 if name.trim().is_empty() || name.contains(['\0', '\n', '\r', ' ']) {
174102 return Err(GitError::InvalidArgument("invalid tag name".to_string()));
175103 }
176 let meta = read_tag_meta(&ctx, &name).await?;
177 let row = git_tags::Entity::find()
178 .filter(git_tags::Column::RepoId.eq(ctx.model.id))
179 .filter(git_tags::Column::Name.eq(&name))
180 .one(self.db_read())
181 .await?;
182 Ok(TagDto {
183 name,
184 tag_type: if meta.is_annotated {
185 TagType::Annotated
186 } else {
187 TagType::Lightweight
188 },
189 target_sha: meta.target_sha,
190 message: meta.message,
191 tagger_name: meta.tagger_name,
192 tagger_email: meta.tagger_email,
193 created_at: row.map(|r| r.created_at),
194 })
104 let baby = self.baby(&ctx);
105 let info = baby.find_tag(&name).await.map_err(map_baby)?;
106 Ok(dto_from_info(&info))
195107 }
196108
197109 pub async fn verify_tag(
@@ -204,7 +116,7 @@
204116 }
205117
206118 pub async fn delete_tag(&self, ctx: &GitContext, name: String) -> Result<(), GitError> {
207 let tag_policy = policy::load_tag_policy(self.database.as_ref(), ctx.model.id).await?;
119 let tag_policy = policy::load_tag_policy(self, ctx).await?;
208120 if tag_policy.is_match(&name) {
209121 return Err(GitError::Forbidden);
210122 }
@@ -212,11 +124,6 @@
212124 baby.delete_tag(&name).await.map_err(map_baby)?;
213125 let key = format!("repo.{}.tag_raw_meta.{}", ctx.model.id, name);
214126 let _ = ctx.cache_remove(&key).await;
215 git_tags::Entity::delete_many()
216 .filter(git_tags::Column::RepoId.eq(ctx.model.id))
217 .filter(git_tags::Column::Name.eq(&name))
218 .exec(self.db_write())
219 .await?;
220127 self.sync_repo_stats(&ctx).await?;
221128 Ok(())
222129 }
Modified lib/share/models/src/repo/repos.rs lib/share/models/src/repo/repos.rs +0 −14 View lib/share/models/src/repo/repos.rs
@@ -14,30 +14,19 @@
1414 pub name: String,
1515 pub description: Option<String>,
1616 pub website: Option<String>,
17 pub default_branch: String,
1817 pub nums_watch: i32,
1918 pub nums_star: i32,
2019 pub nums_pr: i32,
21 pub nums_commits: i32,
22 pub nums_branches: i32,
23 pub nums_tags: i32,
2420 pub enable_lfs: bool,
2521 #[sea_orm(default_value = "false")]
2622 pub is_private: bool,
2723 #[sea_orm(default_value = "false")]
2824 pub is_archived: bool,
2925 pub storage_limit_size: Option<i64>,
30 pub size: i64,
31 pub git_size: i64,
32 pub lfs_size: i64,
3326 pub avatar_url: Option<String>,
34 pub primary_language: Option<String>,
35 pub license_name: Option<String>,
3627 pub created_at: OffsetDateTime,
3728 pub updated_at: OffsetDateTime,
38 pub last_pushed_at: Option<OffsetDateTime>,
3929 pub archived_at: Option<OffsetDateTime>,
40 pub readme_raw: Option<String>,
4130 #[sea_orm(default_value = "true")]
4231 pub allow_rebase: bool,
4332 #[sea_orm(default_value = "false")]
@@ -47,7 +36,6 @@
4736 #[sea_orm(default_value = "true")]
4837 pub allow_merge_commits: bool,
4938 #[sea_orm(default_value = "sha1")]
50 pub object_format: String,
5139 /// 是否允许在本仓库创建合并请求;关闭后合并请求页签对所有人隐藏,
5240 /// 创建接口拒绝执行。
5341 #[sea_orm(default_value = "true")]
@@ -57,8 +45,6 @@
5745 pub tab_order: Option<sea_orm::JsonValue>,
5846 #[sea_orm(belongs_to, from = "dir_id", to = "id")]
5947 pub dir: BelongsTo<super::super::dir::dirs::Entity>,
60 #[sea_orm(belongs_to, from = "license_name", to = "name")]
61 pub license: BelongsTo<Option<super::repo_license::Entity>>,
6248}
6349
6450impl ActiveModelBehavior for ActiveModel {}