From 7a2a3c51c49939f5c83fed0e30041bf7ff962995 Mon Sep 17 00:00:00 2001 From: ZhenYi <434836402@qq.com> Date: Fri, 17 Apr 2026 13:48:05 +0800 Subject: [PATCH] fix(git/hook): make sync_tags Send-safe by collecting git2 data synchronously sync_tags held git2 types (StringArray, Repository) across .await points on sea-orm DB operations. Applied the same pattern as sync_refs: - Added collect_tag_refs() sync helper that collects all tag metadata (name, oid, description, tagger) into owned TagTip structs. - sync_tags now calls collect_tag_refs() before any .await, so no git2 types cross the async boundary. - Removed #[allow(dead_code)] from TagTip now that it's consumed. --- libs/git/hook/sync/commit.rs | 3 +- libs/git/hook/sync/tag.rs | 64 +++++++++++++++++++++++------------- 2 files changed, 43 insertions(+), 24 deletions(-) diff --git a/libs/git/hook/sync/commit.rs b/libs/git/hook/sync/commit.rs index 02ded7d..2277a6c 100644 --- a/libs/git/hook/sync/commit.rs +++ b/libs/git/hook/sync/commit.rs @@ -22,8 +22,7 @@ pub(crate) struct BranchTip { } /// Owned tag data collected from git2 (no git2 types after this). -/// Used by sync_tags; currently populated by collect_git_refs but not yet consumed. -#[allow(dead_code)] +/// Consumed by sync_tags via collect_tag_refs(). #[derive(Debug, Clone)] pub(crate) struct TagTip { pub name: String, diff --git a/libs/git/hook/sync/tag.rs b/libs/git/hook/sync/tag.rs index bf92cec..e96accb 100644 --- a/libs/git/hook/sync/tag.rs +++ b/libs/git/hook/sync/tag.rs @@ -1,3 +1,4 @@ +use crate::hook::sync::commit::TagTip; use crate::GitError; use crate::hook::sync::HookMetaDataSync; use db::database::AppTransaction; @@ -7,21 +8,15 @@ use sea_orm::*; use std::collections::HashSet; impl HookMetaDataSync { - pub async fn sync_tags(&self, txn: &AppTransaction) -> Result<(), GitError> { - let repo_id = self.repo.id; + /// Collect all tag metadata from git2 into owned structs. + /// This is sync and must be called from a `spawn_blocking` context. + pub(crate) fn collect_tag_refs(&self) -> Result, GitError> { let repo = self.domain.repo(); - - let existing: Vec = repo_tag::Entity::find() - .filter(repo_tag::Column::Repo.eq(repo_id)) - .all(txn) - .await - .map_err(|e| GitError::IoError(format!("failed to query tags: {}", e)))?; - let mut existing_names: HashSet = existing.iter().map(|t| t.name.clone()).collect(); - let tag_names = repo .tag_names(None) .map_err(|e| GitError::Internal(e.to_string()))?; + let mut tags = Vec::new(); for tag_name in tag_names.iter().flatten() { let full_ref = format!("refs/tags/{}", tag_name); let reference = match repo.find_reference(&full_ref) { @@ -53,28 +48,53 @@ impl HookMetaDataSync { (None, String::new(), String::new()) }; - if existing_names.contains(tag_name) { - existing_names.remove(tag_name); + tags.push(TagTip { + name: tag_name.to_string(), + target_oid, + description, + tagger_name, + tagger_email, + }); + } + + Ok(tags) + } + + pub async fn sync_tags(&self, txn: &AppTransaction) -> Result<(), GitError> { + let repo_id = self.repo.id; + + let existing: Vec = repo_tag::Entity::find() + .filter(repo_tag::Column::Repo.eq(repo_id)) + .all(txn) + .await + .map_err(|e| GitError::IoError(format!("failed to query tags: {}", e)))?; + let mut existing_names: HashSet = existing.iter().map(|t| t.name.clone()).collect(); + + let tags = self.collect_tag_refs()?; + + for tag in tags { + if existing_names.contains(&tag.name) { + existing_names.remove(&tag.name); repo_tag::Entity::update_many() .filter(repo_tag::Column::Repo.eq(repo_id)) - .filter(repo_tag::Column::Name.eq(tag_name)) - .col_expr(repo_tag::Column::Oid, Expr::value(&target_oid)) - .col_expr(repo_tag::Column::Description, Expr::value(description)) - .col_expr(repo_tag::Column::TaggerName, Expr::value(&tagger_name)) - .col_expr(repo_tag::Column::TaggerEmail, Expr::value(&tagger_email)) + .filter(repo_tag::Column::Name.eq(&tag.name)) + .col_expr(repo_tag::Column::Oid, Expr::value(&tag.target_oid)) + .col_expr(repo_tag::Column::Description, Expr::value(tag.description.clone())) + .col_expr(repo_tag::Column::TaggerName, Expr::value(&tag.tagger_name)) + .col_expr(repo_tag::Column::TaggerEmail, Expr::value(&tag.tagger_email)) .exec(txn) .await .map_err(|e| GitError::IoError(format!("failed to update tag: {}", e)))?; } else { let new_tag = repo_tag::ActiveModel { repo: Set(repo_id), - name: Set(tag_name.to_string()), - oid: Set(target_oid), + name: Set(tag.name.clone()), + oid: Set(tag.target_oid), color: Set(None), - description: Set(description), + description: Set(tag.description.clone()), created_at: Set(chrono::Utc::now()), - tagger_name: Set(tagger_name), - tagger_email: Set(tagger_email), + tagger_name: Set(tag.tagger_name.clone()), + tagger_email: Set(tag.tagger_email.clone()), tagger: Set(None), ..Default::default() };