gitdataai/libs/git/hook/sync/branch.rs
2026-04-15 09:08:09 +08:00

142 lines
5.6 KiB
Rust

use crate::GitError;
use crate::hook::sync::HookMetaDataSync;
use db::database::AppTransaction;
use models::repos::repo;
use models::repos::repo_branch;
use sea_orm::prelude::Expr;
use sea_orm::*;
use std::collections::HashSet;
impl HookMetaDataSync {
pub async fn sync_refs(&self, txn: &AppTransaction) -> Result<(), GitError> {
let repo_id = self.repo.id;
let now = chrono::Utc::now();
let existing: Vec<repo_branch::Model> = repo_branch::Entity::find()
.filter(repo_branch::Column::Repo.eq(repo_id))
.all(txn)
.await
.map_err(|e| GitError::IoError(format!("failed to query branches: {}", e)))?;
let mut existing_names: HashSet<String> = existing.iter().map(|r| r.name.clone()).collect();
let references = self
.domain
.repo()
.references()
.map_err(|e| GitError::Internal(e.to_string()))?;
// Auto-detect first local branch when default_branch is empty
let mut auto_detected_branch: Option<String> = None;
for reference in references {
let reference = reference.map_err(|e| GitError::Internal(e.to_string()))?;
let name = reference
.name()
.ok_or_else(|| GitError::RefNotFound("unnamed ref".into()))?
.to_string();
let shorthand = reference.shorthand().unwrap_or("").to_string();
let target_oid = match reference.target() {
Some(oid) => oid.to_string(),
None => continue,
};
let is_branch = reference.is_branch();
let is_remote = reference.is_remote();
// Detect first local branch if no default is set
if self.repo.default_branch.is_empty()
&& is_branch
&& !is_remote
&& auto_detected_branch.is_none()
{
auto_detected_branch = Some(shorthand.clone());
}
let upstream = if is_branch && !is_remote {
reference
.shorthand()
.map(|short| format!("refs/remotes/{{}}/{}", short))
} else {
None
};
if existing_names.contains(&name) {
existing_names.remove(&name);
repo_branch::Entity::update_many()
.filter(repo_branch::Column::Repo.eq(repo_id))
.filter(repo_branch::Column::Name.eq(&name))
.col_expr(repo_branch::Column::Oid, Expr::value(&target_oid))
.col_expr(repo_branch::Column::Upstream, Expr::value(upstream))
.col_expr(
repo_branch::Column::Head,
Expr::value(is_branch && shorthand == self.repo.default_branch),
)
.col_expr(repo_branch::Column::UpdatedAt, Expr::value(now))
.exec(txn)
.await
.map_err(|e| GitError::IoError(format!("failed to update branch: {}", e)))?;
} else {
let new_branch = repo_branch::ActiveModel {
repo: Set(repo_id),
name: Set(name),
oid: Set(target_oid),
upstream: Set(upstream),
head: Set(is_branch && shorthand == self.repo.default_branch),
created_at: Set(now),
updated_at: Set(now),
..Default::default()
};
new_branch
.insert(txn)
.await
.map_err(|e| GitError::IoError(format!("failed to insert branch: {}", e)))?;
}
}
if !existing_names.is_empty() {
repo_branch::Entity::delete_many()
.filter(repo_branch::Column::Repo.eq(repo_id))
.filter(repo_branch::Column::Name.is_in(existing_names))
.exec(txn)
.await
.map_err(|e| {
GitError::IoError(format!("failed to delete stale branches: {}", e))
})?;
}
// Persist auto-detected default branch and update head flags
if let Some(ref branch_name) = auto_detected_branch {
// 1. Update the repo's default_branch
repo::Entity::update_many()
.filter(repo::Column::Id.eq(repo_id))
.col_expr(
repo::Column::DefaultBranch,
Expr::value(branch_name.clone()),
)
.exec(txn)
.await
.map_err(|e| GitError::IoError(format!("failed to set default branch: {}", e)))?;
// 2. Clear head on all branches
repo_branch::Entity::update_many()
.filter(repo_branch::Column::Repo.eq(repo_id))
.col_expr(repo_branch::Column::Head, Expr::value(false))
.exec(txn)
.await
.map_err(|e| GitError::IoError(format!("failed to clear head flags: {}", e)))?;
// 3. Set head = true for the detected branch (it was inserted above)
repo_branch::Entity::update_many()
.filter(repo_branch::Column::Repo.eq(repo_id))
.filter(repo_branch::Column::Name.eq(branch_name))
.col_expr(repo_branch::Column::Head, Expr::value(true))
.exec(txn)
.await
.map_err(|e| GitError::IoError(format!("failed to set head flag: {}", e)))?;
}
Ok(())
}
}