89109d2
use std::collections::HashMap;
use crate::rpcgit::branch::ListBranchesOptions;
use models::git::git_branches;
use models::repo::repos;
use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, QueryFilter, Set, Unchanged};
use serde::{Deserialize, Serialize};
use time::OffsetDateTime;
use crate::context::facade::GitContext;
use crate::service::GitService;
use crate::share::errors::{GitError, map_baby};
use crate::share::policy::load_branch_policy;
use crate::share::validate::validate_branch_name;
#[derive(Debug, Clone, Serialize, utoipa::ToSchema)]
pub struct BranchDto {
pub name: String,
pub commit_sha: String,
pub is_protected: bool,
pub is_deleted: bool,
pub deleted_at: Option<OffsetDateTime>,
pub created_at: OffsetDateTime,
pub updated_at: OffsetDateTime,
}
impl BranchDto {
fn from_row(row: &git_branches::Model, sha: String) -> Self {
Self {
name: row.name.clone(),
commit_sha: sha,
is_protected: row.is_protected,
is_deleted: row.is_deleted,
deleted_at: row.deleted_at,
created_at: row.created_at,
updated_at: row.updated_at,
}
}
}
#[derive(Debug, Clone, Deserialize, utoipa::ToSchema)]
pub struct CreateBranchCmd {
pub name: String,
#[serde(default)]
pub start_point: Option<String>,
}
impl GitService {
async fn branch_rows(
&self,
ctx: &GitContext,
) -> Result<HashMap<String, git_branches::Model>, GitError> {
let rows = git_branches::Entity::find()
.filter(git_branches::Column::RepoId.eq(ctx.model.id))
.all(self.db_read())
.await?;
Ok(rows.into_iter().map(|r| (r.name.clone(), r)).collect())
}
pub async fn list_branches(
&self,
space_name: String,
repo_name: String,
) -> Result<Vec<BranchDto>, GitError> {
let ctx = self.context(space_name, repo_name).await?;
if ctx.repo_uninitialized() {
return Ok(Vec::new());
}
let baby = self.baby(&ctx);
let branches = baby
.list_branches(ListBranchesOptions::default())
.await
.map_err(map_baby)?;
let mut rows = self.branch_rows(&ctx).await?;
let policy = load_branch_policy(&self.state.database, ctx.model.id).await?;
for b in &branches {
if !rows.contains_key(&b.name) && !b.name.is_empty() {
let now = OffsetDateTime::now_utc();
let row = git_branches::ActiveModel {
id: Set(uuid::Uuid::now_v7()),
repo_id: Set(ctx.model.id),
name: Set(b.name.clone()),
commit_sha: Set(b.sha.to_string()),
is_deleted: Set(false),
is_protected: Set(policy.is_match(&b.name)),
deleted_at: Set(None),
created_at: Set(now),
updated_at: Set(now),
};
let inserted = row.insert(self.db_write()).await?;
rows.insert(inserted.name.clone(), inserted);
} else if let Some(row) = rows.get(&b.name) {
let protected = policy.is_match(&b.name);
if row.is_protected != protected {
let am = git_branches::ActiveModel {
id: Unchanged(row.id),
is_protected: Set(protected),
updated_at: Set(OffsetDateTime::now_utc()),
..Default::default()
};
if let Ok(updated) = am.update(self.db_write()).await {
rows.insert(updated.name.clone(), updated);
}
}
}
}
let mut out: Vec<BranchDto> = branches
.iter()
.filter_map(|b| {
rows.get(&b.name)
.map(|row| BranchDto::from_row(row, b.sha.to_string()))
})
.collect();
out.sort_by(|a, b| a.name.cmp(&b.name));
Ok(out)
}
pub async fn get_branch(
&self,
space_name: String,
repo_name: String,
name: String,
) -> Result<BranchDto, GitError> {
validate_branch_name(&name)?;
let ctx = self.context(space_name, repo_name).await?;
let baby = self.baby(&ctx);
let info = baby.find_branch(&name).await.map_err(map_baby)?;
let rows = self.branch_rows(&ctx).await?;
match rows.get(&name) {
Some(row) => Ok(BranchDto::from_row(row, info.sha.to_string())),
None => Ok(BranchDto {
name: info.name,
commit_sha: info.sha.to_string(),
is_protected: false,
is_deleted: false,
deleted_at: None,
created_at: OffsetDateTime::now_utc(),
updated_at: OffsetDateTime::now_utc(),
}),
}
}
pub async fn create_branch(
&self,
space_name: String,
repo_name: String,
cmd: CreateBranchCmd,
) -> Result<BranchDto, GitError> {
let name = validate_branch_name(&cmd.name)?;
if let Some(sp) = &cmd.start_point
&& (sp.trim().is_empty() || sp.starts_with('-'))
{
return Err(GitError::InvalidArgument(format!(
"invalid start point `{sp}`"
)));
}
let ctx = self.context(space_name, repo_name).await?;
let baby = self.baby(&ctx);
baby.create_branch(&name, cmd.start_point.as_deref())
.await
.map_err(map_baby)?;
let info = baby.find_branch(&name).await.map_err(map_baby)?;
let rows = self.branch_rows(&ctx).await?;
let now = OffsetDateTime::now_utc();
let row = match rows.get(&name) {
Some(existing) => {
let am = git_branches::ActiveModel {
id: Unchanged(existing.id),
commit_sha: Set(info.sha.to_string()),
is_deleted: Set(false),
deleted_at: Set(None),
updated_at: Set(now),
..Default::default()
};
am.update(self.db_write()).await?
}
None => {
let am = git_branches::ActiveModel {
id: Set(uuid::Uuid::now_v7()),
repo_id: Set(ctx.model.id),
name: Set(name.clone()),
commit_sha: Set(info.sha.to_string()),
is_deleted: Set(false),
is_protected: Set(false),
deleted_at: Set(None),
created_at: Set(now),
updated_at: Set(now),
};
am.insert(self.db_write()).await?
}
};
self.sync_repo_stats(&ctx).await?;
Ok(BranchDto::from_row(&row, info.sha.to_string()))
}
pub async fn rename_branch(
&self,
space_name: String,
repo_name: String,
from: String,
to: String,
) -> Result<(), GitError> {
validate_branch_name(&from)?;
let to = validate_branch_name(&to)?;
let ctx = self.context(space_name, repo_name).await?;
let baby = self.baby(&ctx);
baby.rename_branch(&from, &to).await.map_err(map_baby)?;
let product = git_branches::Entity::find()
.filter(git_branches::Column::RepoId.eq(ctx.model.id))
.filter(git_branches::Column::Name.eq(&from))
.one(self.db_write())
.await?;
if let Some(row) = product {
let am = git_branches::ActiveModel {
id: Unchanged(row.id),
name: Set(to),
updated_at: Set(OffsetDateTime::now_utc()),
..Default::default()
};
am.update(self.db_write()).await?;
}
self.sync_repo_stats(&ctx).await?;
Ok(())
}
pub async fn delete_branch(
&self,
space_name: String,
repo_name: String,
name: String,
force: bool,
) -> Result<(), GitError> {
let name = validate_branch_name(&name)?;
let ctx = self.context(space_name, repo_name).await?;
let policy = load_branch_policy(&self.state.database, ctx.model.id).await?;
if policy.is_match(&name) {
return Err(GitError::Forbidden);
}
let baby = self.baby(&ctx);
let info = baby.find_branch(&name).await.map_err(map_baby)?;
baby.delete_branch(&name, force).await.map_err(map_baby)?;
let now = OffsetDateTime::now_utc();
let rows = self.branch_rows(&ctx).await?;
match rows.get(&name) {
Some(existing) => {
let am = git_branches::ActiveModel {
id: Unchanged(existing.id),
is_deleted: Set(true),
deleted_at: Set(Some(now)),
updated_at: Set(now),
..Default::default()
};
am.update(self.db_write()).await?;
}
None => {
let am = git_branches::ActiveModel {
id: Set(uuid::Uuid::now_v7()),
repo_id: Set(ctx.model.id),
name: Set(name.clone()),
commit_sha: Set(info.sha.to_string()),
is_deleted: Set(true),
is_protected: Set(false),
deleted_at: Set(Some(now)),
created_at: Set(now),
updated_at: Set(now),
};
am.insert(self.db_write()).await?;
}
}
self.sync_repo_stats(&ctx).await?;
Ok(())
}
pub async fn set_default_branch(
&self,
space_name: String,
repo_name: String,
name: String,
) -> Result<(), GitError> {
let name = validate_branch_name(&name)?;
let ctx = self.context(space_name, repo_name).await?;
let baby = self.baby(&ctx);
baby.set_default_branch(&name).await.map_err(map_baby)?;
let am = repos::ActiveModel {
id: Unchanged(ctx.model.id),
default_branch: Set(name),
updated_at: Set(OffsetDateTime::now_utc()),
..Default::default()
};
am.update(self.db_write()).await?;
Ok(())
}
}