Skip to content
Clone
80f9e01
176 lines 5.2 KB Rust
use std::collections::HashMap;
use gitbaby::GitBaby;
use gitbaby::branch::ListBranchesOptions;
use models::git::git_branches;
use models::repo::repos;
use sea_orm::{
ActiveModelTrait, ColumnTrait, DatabaseTransaction, EntityTrait, IntoActiveModel, QueryFilter,
Set,
};
use time::OffsetDateTime;
use uuid::Uuid;
use crate::sync::error::{SyncError, SyncResult};
const PREFERRED_DEFAULT_BRANCHES: &[&str] = &["main", "master", "trunk"];
#[derive(Debug, Clone)]
pub(crate) struct BranchTip {
pub name: String,
pub target_sha: String,
}
pub async fn sync_branches(
baby: &GitBaby,
txn: &DatabaseTransaction,
repo: &repos::Model,
) -> SyncResult<usize> {
let collected = collect_branch_tips(baby).await?;
let existing = git_branches::Entity::find()
.filter(git_branches::Column::RepoId.eq(repo.id))
.all(txn)
.await
.map_err(SyncError::Db)?;
let mut existing_by_name: HashMap<String, git_branches::Model> = existing
.into_iter()
.filter(|b| !b.is_deleted)
.map(|b| (b.name.clone(), b))
.collect();
tracing::debug!(
repo_id = %repo.id,
tips = collected.len(),
db_branches = existing_by_name.len(),
"syncing branches"
);
let mut stale: std::collections::HashSet<String> = existing_by_name.keys().cloned().collect();
let (mut updated, mut inserted) = (0usize, 0usize);
for tip in &collected {
stale.remove(&tip.name);
match existing_by_name.get(&tip.name) {
Some(row) if row.commit_sha != tip.target_sha => {
let mut am = row.clone().into_active_model();
am.commit_sha = Set(tip.target_sha.clone());
am.updated_at = Set(OffsetDateTime::now_utc());
am.update(txn).await.map_err(SyncError::Db)?;
updated += 1;
}
Some(_) => {}
None => {
let now = OffsetDateTime::now_utc();
git_branches::ActiveModel {
id: Set(Uuid::now_v7()),
repo_id: Set(repo.id),
name: Set(tip.name.clone()),
commit_sha: Set(tip.target_sha.clone()),
is_deleted: Set(false),
is_protected: Set(false),
deleted_at: Set(None),
created_at: Set(now),
updated_at: Set(now),
}
.insert(txn)
.await
.map_err(SyncError::Db)?;
inserted += 1;
}
}
}
let mut deleted = 0usize;
for name in stale {
if existing_by_name.get(&name).is_some_and(|r| r.is_protected) {
tracing::debug!(branch = %name, "stale branch is protected; keeping row");
continue;
}
let Some(row) = existing_by_name.remove(&name) else {
continue;
};
let mut am = row.into_active_model();
am.is_deleted = Set(true);
am.deleted_at = Set(Some(OffsetDateTime::now_utc()));
am.updated_at = Set(OffsetDateTime::now_utc());
am.update(txn).await.map_err(SyncError::Db)?;
deleted += 1;
}
tracing::debug!(
repo_id = %repo.id,
updated,
inserted,
deleted,
"branch sync wrote changes"
);
reconcile_default_branch(baby, txn, repo, &collected).await?;
Ok(collected.len())
}
pub(crate) async fn collect_branch_tips(baby: &GitBaby) -> SyncResult<Vec<BranchTip>> {
let branches = baby
.list_branches(ListBranchesOptions::default())
.await
.map_err(SyncError::Git)?;
Ok(branches
.into_iter()
.map(|b| BranchTip {
name: b.name,
target_sha: b.sha.to_string(),
})
.collect())
}
async fn reconcile_default_branch(
baby: &GitBaby,
txn: &DatabaseTransaction,
repo: &repos::Model,
collected: &[BranchTip],
) -> SyncResult<()> {
if collected.is_empty() {
return Ok(());
}
let desired = if collected.iter().any(|b| b.name == repo.default_branch) {
repo.default_branch.clone()
} else {
PREFERRED_DEFAULT_BRANCHES
.iter()
.copied()
.find(|name| collected.iter().any(|b| b.name == *name))
.or_else(|| collected.first().map(|b| b.name.as_str()))
.unwrap_or(&repo.default_branch)
.to_string()
};
if desired != repo.default_branch {
tracing::info!(
repo_id = %repo.id,
old = %repo.default_branch,
new = %desired,
"default branch changed during sync"
);
let mut am = repo.clone().into_active_model();
am.default_branch = Set(desired.clone());
am.update(txn).await.map_err(SyncError::Db)?;
}
let head_ok = matches!(
baby.get_default_branch().await,
Ok(current) if current == desired
);
if !head_ok && let Err(e) = baby.set_default_branch(&desired).await {
tracing::warn!(
repo_id = %repo.id,
branch = %desired,
error = %e,
"failed to update on-disk HEAD symbolic-ref"
);
}
Ok(())
}