916 lines
28 KiB
Rust
916 lines
28 KiB
Rust
use crate::AppService;
|
|
use crate::error::AppError;
|
|
use crate::git::{BranchDiff, BranchInfo, BranchSummary};
|
|
use models::repos::repo;
|
|
use models::repos::repo as repo_model;
|
|
use models::repos::repo_branch;
|
|
use models::repos::repo_branch_protect;
|
|
use sea_orm::prelude::Expr;
|
|
use sea_orm::{ColumnTrait, EntityTrait, ExprTrait, PaginatorTrait, QueryFilter};
|
|
use serde::{Deserialize, Serialize};
|
|
use session::Session;
|
|
use uuid::Uuid;
|
|
|
|
#[derive(Debug, Clone, Deserialize, utoipa::ToSchema)]
|
|
pub struct BranchListQuery {
|
|
#[serde(default)]
|
|
pub remote_only: Option<bool>,
|
|
#[serde(default)]
|
|
pub all: Option<bool>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, utoipa::ToSchema)]
|
|
pub struct BranchInfoResponse {
|
|
pub name: String,
|
|
pub oid: String,
|
|
pub is_head: bool,
|
|
pub is_remote: bool,
|
|
pub is_current: bool,
|
|
pub upstream: Option<String>,
|
|
}
|
|
|
|
impl From<BranchInfo> for BranchInfoResponse {
|
|
fn from(b: BranchInfo) -> Self {
|
|
Self {
|
|
name: b.name,
|
|
oid: b.oid.to_string(),
|
|
is_head: b.is_head,
|
|
is_remote: b.is_remote,
|
|
is_current: b.is_current,
|
|
upstream: b.upstream,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl From<repo_branch::Model> for BranchInfoResponse {
|
|
fn from(b: repo_branch::Model) -> Self {
|
|
// is_remote: full ref path starts with "refs/remotes/"
|
|
let is_remote = b.name.starts_with("refs/remotes/");
|
|
// shorthand name for display (strip prefix)
|
|
let name = if b.name.starts_with("refs/heads/") {
|
|
b.name
|
|
.strip_prefix("refs/heads/")
|
|
.unwrap_or(&b.name)
|
|
.to_string()
|
|
} else if b.name.starts_with("refs/remotes/") {
|
|
b.name
|
|
.strip_prefix("refs/remotes/")
|
|
.unwrap_or(&b.name)
|
|
.to_string()
|
|
} else {
|
|
b.name.clone()
|
|
};
|
|
Self {
|
|
name,
|
|
oid: b.oid,
|
|
is_head: b.head,
|
|
is_remote,
|
|
// is_current: not stored in DB, always false when from DB
|
|
is_current: false,
|
|
// upstream: stored as pattern "refs/remotes/{}/{}", return as-is
|
|
upstream: b.upstream,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, utoipa::ToSchema)]
|
|
pub struct BranchSummaryResponse {
|
|
pub local_count: usize,
|
|
pub remote_count: usize,
|
|
pub all_count: usize,
|
|
}
|
|
|
|
impl From<BranchSummary> for BranchSummaryResponse {
|
|
fn from(s: BranchSummary) -> Self {
|
|
Self {
|
|
local_count: s.local_count,
|
|
remote_count: s.remote_count,
|
|
all_count: s.all_count,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, utoipa::ToSchema)]
|
|
pub struct BranchDiffResponse {
|
|
pub ahead: usize,
|
|
pub behind: usize,
|
|
pub diverged: bool,
|
|
}
|
|
|
|
impl From<BranchDiff> for BranchDiffResponse {
|
|
fn from(d: BranchDiff) -> Self {
|
|
Self {
|
|
ahead: d.ahead,
|
|
behind: d.behind,
|
|
diverged: d.diverged,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, utoipa::ToSchema)]
|
|
pub struct BranchExistsResponse {
|
|
pub name: String,
|
|
pub exists: bool,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, utoipa::ToSchema)]
|
|
pub struct BranchIsHeadResponse {
|
|
pub name: String,
|
|
pub is_head: bool,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, utoipa::ToSchema)]
|
|
pub struct BranchIsDetachedResponse {
|
|
pub is_detached: bool,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, utoipa::ToSchema)]
|
|
pub struct BranchIsMergedResponse {
|
|
pub branch: String,
|
|
pub into: String,
|
|
pub is_merged: bool,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, utoipa::ToSchema)]
|
|
pub struct BranchMergeBaseResponse {
|
|
pub branch1: String,
|
|
pub branch2: String,
|
|
pub base: String,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, utoipa::ToSchema)]
|
|
pub struct BranchIsAncestorResponse {
|
|
pub child: String,
|
|
pub ancestor: String,
|
|
pub is_ancestor: bool,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, utoipa::ToSchema)]
|
|
pub struct BranchFastForwardResponse {
|
|
pub oid: String,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, utoipa::ToSchema)]
|
|
pub struct BranchTrackingDiffResponse {
|
|
pub name: String,
|
|
pub ahead: usize,
|
|
pub behind: usize,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, utoipa::ToSchema)]
|
|
pub struct BranchIsConflictedResponse {
|
|
pub is_conflicted: bool,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Deserialize, utoipa::ToSchema)]
|
|
pub struct BranchCreateRequest {
|
|
pub name: String,
|
|
pub oid: Option<String>,
|
|
#[serde(default)]
|
|
pub force: bool,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Deserialize, utoipa::ToSchema)]
|
|
pub struct BranchRenameRequest {
|
|
pub old_name: String,
|
|
pub new_name: String,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Deserialize, utoipa::ToSchema)]
|
|
pub struct BranchMoveRequest {
|
|
pub name: String,
|
|
pub new_name: String,
|
|
#[serde(default)]
|
|
pub force: bool,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Deserialize, utoipa::ToSchema)]
|
|
pub struct BranchSetUpstreamRequest {
|
|
pub name: String,
|
|
pub upstream: Option<String>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Deserialize, utoipa::ToSchema)]
|
|
pub struct BranchDiffQuery {
|
|
pub local: String,
|
|
pub remote: String,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Deserialize, utoipa::ToSchema)]
|
|
pub struct BranchIsMergedQuery {
|
|
pub branch: String,
|
|
pub into: String,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Deserialize, utoipa::ToSchema)]
|
|
pub struct BranchIsAncestorQuery {
|
|
pub child: String,
|
|
pub ancestor: String,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Deserialize, utoipa::ToSchema)]
|
|
pub struct BranchMergeBaseQuery {
|
|
pub branch1: String,
|
|
pub branch2: String,
|
|
}
|
|
|
|
macro_rules! git_spawn {
|
|
($repo:expr, $domain:ident -> $body:expr) => {{
|
|
let repo_clone = $repo.clone();
|
|
tokio::task::spawn_blocking(move || {
|
|
let $domain = git::GitDomain::from_model(repo_clone)?;
|
|
$body
|
|
})
|
|
.await
|
|
.map_err(|e| AppError::InternalServerError(format!("Task join error: {}", e)))?
|
|
.map_err(AppError::from)
|
|
}};
|
|
}
|
|
|
|
impl AppService {
|
|
/// Check and enforce branch protection rules before deleting (or renaming/moving away from) a branch.
|
|
async fn check_protection_for_deletion(
|
|
&self,
|
|
repo_id: Uuid,
|
|
branch: &str,
|
|
) -> Result<(), AppError> {
|
|
let protection = repo_branch_protect::Entity::find()
|
|
.filter(repo_branch_protect::Column::Repo.eq(repo_id))
|
|
.filter(repo_branch_protect::Column::Branch.eq(branch))
|
|
.one(&self.db)
|
|
.await
|
|
.map_err(AppError::from)?;
|
|
|
|
if let Some(rule) = protection {
|
|
if rule.forbid_deletion {
|
|
return Err(AppError::Forbidden(format!(
|
|
"Deletion of protected branch '{}' is forbidden",
|
|
branch
|
|
)));
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// Check and enforce branch protection rules before creating (or renaming/moving to) a branch.
|
|
async fn check_protection_for_push(&self, repo_id: Uuid, branch: &str) -> Result<(), AppError> {
|
|
let protection = repo_branch_protect::Entity::find()
|
|
.filter(repo_branch_protect::Column::Repo.eq(repo_id))
|
|
.filter(repo_branch_protect::Column::Branch.eq(branch))
|
|
.one(&self.db)
|
|
.await
|
|
.map_err(AppError::from)?;
|
|
|
|
if let Some(rule) = protection {
|
|
if rule.forbid_push {
|
|
return Err(AppError::Forbidden(format!(
|
|
"Push to protected branch '{}' is forbidden",
|
|
branch
|
|
)));
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
pub async fn git_branch_list(
|
|
&self,
|
|
namespace: String,
|
|
repo_name: String,
|
|
query: BranchListQuery,
|
|
ctx: &Session,
|
|
) -> Result<Vec<BranchInfoResponse>, AppError> {
|
|
let repo = self.utils_find_repo(namespace, repo_name, ctx).await?;
|
|
|
|
let branches = repo_branch::Entity::find()
|
|
.filter(repo_branch::Column::Repo.eq(repo.id))
|
|
.filter({
|
|
if query.all.unwrap_or(false) {
|
|
// all: no filter
|
|
Expr::value(true)
|
|
} else if query.remote_only.unwrap_or(false) {
|
|
Expr::col(repo_branch::Column::Name).like("refs/remotes/%")
|
|
} else {
|
|
Expr::col(repo_branch::Column::Name).like("refs/heads/%")
|
|
}
|
|
})
|
|
.all(&self.db)
|
|
.await
|
|
.map_err(AppError::from)?;
|
|
|
|
Ok(branches.into_iter().map(BranchInfoResponse::from).collect())
|
|
}
|
|
|
|
pub async fn git_branch_summary(
|
|
&self,
|
|
namespace: String,
|
|
repo_name: String,
|
|
ctx: &Session,
|
|
) -> Result<BranchSummaryResponse, AppError> {
|
|
let repo = self.utils_find_repo(namespace, repo_name, ctx).await?;
|
|
|
|
let local_count: usize = repo_branch::Entity::find()
|
|
.filter(repo_branch::Column::Repo.eq(repo.id))
|
|
.filter(Expr::col(repo_branch::Column::Name).like("refs/heads/%"))
|
|
.count(&self.db)
|
|
.await
|
|
.map_err(AppError::from)? as usize;
|
|
|
|
let remote_count: usize = repo_branch::Entity::find()
|
|
.filter(repo_branch::Column::Repo.eq(repo.id))
|
|
.filter(Expr::col(repo_branch::Column::Name).like("refs/remotes/%"))
|
|
.count(&self.db)
|
|
.await
|
|
.map_err(AppError::from)? as usize;
|
|
|
|
Ok(BranchSummaryResponse {
|
|
local_count,
|
|
remote_count,
|
|
all_count: local_count + remote_count,
|
|
})
|
|
}
|
|
|
|
pub async fn git_branch_get(
|
|
&self,
|
|
namespace: String,
|
|
repo_name: String,
|
|
name: String,
|
|
ctx: &Session,
|
|
) -> Result<BranchInfoResponse, AppError> {
|
|
let repo = self.utils_find_repo(namespace, repo_name, ctx).await?;
|
|
|
|
// Normalize: try shorthand forms, then full ref paths
|
|
let candidates = if name.starts_with("refs/") {
|
|
vec![name.clone()]
|
|
} else if name.starts_with("heads/") {
|
|
vec![format!("refs/{}", name)]
|
|
} else {
|
|
vec![
|
|
format!("refs/heads/{}", name),
|
|
format!("refs/remotes/{}", name),
|
|
name.clone(),
|
|
]
|
|
};
|
|
|
|
for candidate in &candidates {
|
|
if let Some(b) = repo_branch::Entity::find()
|
|
.filter(repo_branch::Column::Repo.eq(repo.id))
|
|
.filter(repo_branch::Column::Name.eq(candidate))
|
|
.one(&self.db)
|
|
.await
|
|
.map_err(AppError::from)?
|
|
{
|
|
return Ok(BranchInfoResponse::from(b));
|
|
}
|
|
}
|
|
|
|
// Fallback to git
|
|
let name_clone = name.clone();
|
|
let info = git_spawn!(repo, domain -> {
|
|
domain.branch_get(&name_clone)
|
|
})?;
|
|
Ok(BranchInfoResponse::from(info))
|
|
}
|
|
|
|
pub async fn git_branch_current(
|
|
&self,
|
|
namespace: String,
|
|
repo_name: String,
|
|
ctx: &Session,
|
|
) -> Result<Option<BranchInfoResponse>, AppError> {
|
|
let repo = self.utils_find_repo(namespace, repo_name, ctx).await?;
|
|
|
|
// Try git first (normal path). Errors (e.g. empty repo) fall through to DB fallback.
|
|
if let Ok(Some(b)) = git_spawn!(repo, domain -> {
|
|
domain.branch_current()
|
|
}) {
|
|
return Ok(Some(BranchInfoResponse::from(b)));
|
|
}
|
|
|
|
// Fallback: repo may be empty or sync not yet run, but default_branch
|
|
// is already set in repo metadata. Look it up in DB.
|
|
// repo.default_branch is shorthand (e.g. "main"), DB stores full ref (e.g. "refs/heads/main")
|
|
if !repo.default_branch.is_empty() {
|
|
let full_ref = format!("refs/heads/{}", repo.default_branch);
|
|
if let Some(branch) = repo_branch::Entity::find()
|
|
.filter(repo_branch::Column::Repo.eq(repo.id))
|
|
.filter(repo_branch::Column::Name.eq(&full_ref))
|
|
.one(&self.db)
|
|
.await
|
|
.map_err(AppError::from)?
|
|
{
|
|
return Ok(Some(BranchInfoResponse::from(branch)));
|
|
}
|
|
}
|
|
|
|
Ok(None)
|
|
}
|
|
|
|
pub async fn git_branch_exists(
|
|
&self,
|
|
namespace: String,
|
|
repo_name: String,
|
|
name: String,
|
|
ctx: &Session,
|
|
) -> Result<BranchExistsResponse, AppError> {
|
|
let repo = self.utils_find_repo(namespace, repo_name, ctx).await?;
|
|
|
|
// Try shorthand → full ref path candidates
|
|
let candidates = if name.starts_with("refs/") {
|
|
vec![name.clone()]
|
|
} else {
|
|
vec![
|
|
format!("refs/heads/{}", name),
|
|
format!("refs/remotes/{}", name),
|
|
name.clone(),
|
|
]
|
|
};
|
|
|
|
for candidate in &candidates {
|
|
let found = repo_branch::Entity::find()
|
|
.filter(repo_branch::Column::Repo.eq(repo.id))
|
|
.filter(repo_branch::Column::Name.eq(candidate))
|
|
.one(&self.db)
|
|
.await
|
|
.map_err(AppError::from)?;
|
|
if found.is_some() {
|
|
return Ok(BranchExistsResponse { name, exists: true });
|
|
}
|
|
}
|
|
|
|
// Fallback to git
|
|
let name_clone = name.clone();
|
|
let exists = git_spawn!(repo, domain -> {
|
|
Ok::<_, git::GitError>(domain.branch_exists(&name_clone))
|
|
})?;
|
|
|
|
Ok(BranchExistsResponse { name, exists })
|
|
}
|
|
|
|
pub async fn git_branch_is_head(
|
|
&self,
|
|
namespace: String,
|
|
repo_name: String,
|
|
name: String,
|
|
ctx: &Session,
|
|
) -> Result<BranchIsHeadResponse, AppError> {
|
|
let repo = self.utils_find_repo(namespace, repo_name, ctx).await?;
|
|
let name_clone = name.clone();
|
|
|
|
let is_head = git_spawn!(repo, domain -> {
|
|
domain.branch_is_head(&name_clone)
|
|
})?;
|
|
|
|
Ok(BranchIsHeadResponse { name, is_head })
|
|
}
|
|
|
|
pub async fn git_branch_is_detached(
|
|
&self,
|
|
namespace: String,
|
|
repo_name: String,
|
|
ctx: &Session,
|
|
) -> Result<BranchIsDetachedResponse, AppError> {
|
|
let repo = self.utils_find_repo(namespace, repo_name, ctx).await?;
|
|
|
|
let is_detached = git_spawn!(repo, domain -> {
|
|
Ok::<_, git::GitError>(domain.branch_is_detached())
|
|
})?;
|
|
|
|
Ok(BranchIsDetachedResponse { is_detached })
|
|
}
|
|
|
|
pub async fn git_branch_upstream(
|
|
&self,
|
|
namespace: String,
|
|
repo_name: String,
|
|
name: String,
|
|
ctx: &Session,
|
|
) -> Result<Option<BranchInfoResponse>, AppError> {
|
|
let repo = self.utils_find_repo(namespace, repo_name, ctx).await?;
|
|
let name_clone = name.clone();
|
|
|
|
let upstream = git_spawn!(repo, domain -> {
|
|
domain.branch_upstream(&name_clone)
|
|
})?;
|
|
|
|
Ok(upstream.map(BranchInfoResponse::from))
|
|
}
|
|
|
|
pub async fn git_branch_diff(
|
|
&self,
|
|
namespace: String,
|
|
repo_name: String,
|
|
query: BranchDiffQuery,
|
|
ctx: &Session,
|
|
) -> Result<BranchDiffResponse, AppError> {
|
|
let repo = self.utils_find_repo(namespace, repo_name, ctx).await?;
|
|
let local = query.local.clone();
|
|
let remote = query.remote.clone();
|
|
|
|
let diff = git_spawn!(repo, domain -> {
|
|
domain.branch_diff(&local, &remote)
|
|
})?;
|
|
|
|
Ok(BranchDiffResponse::from(diff))
|
|
}
|
|
|
|
pub async fn git_branch_tracking_difference(
|
|
&self,
|
|
namespace: String,
|
|
repo_name: String,
|
|
name: String,
|
|
ctx: &Session,
|
|
) -> Result<BranchTrackingDiffResponse, AppError> {
|
|
let repo = self.utils_find_repo(namespace, repo_name, ctx).await?;
|
|
let name_clone = name.clone();
|
|
|
|
let (ahead, behind) = git_spawn!(repo, domain -> {
|
|
domain.branch_tracking_difference(&name_clone)
|
|
})?;
|
|
|
|
Ok(BranchTrackingDiffResponse {
|
|
name,
|
|
ahead,
|
|
behind,
|
|
})
|
|
}
|
|
|
|
pub async fn git_branch_create(
|
|
&self,
|
|
namespace: String,
|
|
repo_name: String,
|
|
request: BranchCreateRequest,
|
|
ctx: &Session,
|
|
) -> Result<BranchInfoResponse, AppError> {
|
|
let repo: repo::Model = self
|
|
.utils_check_repo_admin(namespace.clone(), repo_name.clone(), ctx)
|
|
.await?;
|
|
self.check_protection_for_push(repo.id, &request.name)
|
|
.await?;
|
|
let name = request.name.clone();
|
|
let force = request.force;
|
|
let name_for_spawn = name.clone();
|
|
|
|
let info = if let Some(oid) = request.oid {
|
|
let commit_oid = git::CommitOid::new(&oid);
|
|
let name_clone = name_for_spawn.clone();
|
|
git_spawn!(repo, domain -> {
|
|
domain.branch_create(&name_clone, &commit_oid, force)
|
|
})?
|
|
} else {
|
|
git_spawn!(repo, domain -> {
|
|
domain.branch_create_from_head(&name_for_spawn, force)
|
|
})?
|
|
};
|
|
|
|
let response = BranchInfoResponse::from(info);
|
|
|
|
let project_id = match repo_model::Entity::find_by_id(repo.id).one(&self.db).await {
|
|
Ok(Some(r)) => r.project,
|
|
Ok(None) => Uuid::nil(),
|
|
Err(e) => {
|
|
slog::warn!(
|
|
self.logs,
|
|
"failed to look up project_id for activity log: {}",
|
|
e
|
|
);
|
|
Uuid::nil()
|
|
}
|
|
};
|
|
let user_uid = ctx.user().unwrap_or(Uuid::nil());
|
|
let _ = self
|
|
.project_log_activity(
|
|
project_id,
|
|
Some(repo.id),
|
|
user_uid,
|
|
super::super::project::activity::ActivityLogParams {
|
|
event_type: "branch_create".to_string(),
|
|
title: format!("{} created branch '{}'", user_uid, name),
|
|
repo_id: Some(repo.id),
|
|
content: None,
|
|
event_id: None,
|
|
event_sub_id: None,
|
|
metadata: Some(serde_json::json!({"branch_name": name})),
|
|
is_private: false,
|
|
},
|
|
)
|
|
.await;
|
|
|
|
Ok(response)
|
|
}
|
|
|
|
pub async fn git_branch_delete(
|
|
&self,
|
|
namespace: String,
|
|
repo_name: String,
|
|
name: String,
|
|
ctx: &Session,
|
|
) -> Result<(), AppError> {
|
|
let repo: repo::Model = self
|
|
.utils_check_repo_admin(namespace.clone(), repo_name.clone(), ctx)
|
|
.await?;
|
|
self.check_protection_for_deletion(repo.id, &name).await?;
|
|
let name_for_spawn = name.clone();
|
|
|
|
git_spawn!(repo, domain -> {
|
|
domain.branch_delete(&name_for_spawn)
|
|
})?;
|
|
|
|
let project_id = match repo_model::Entity::find_by_id(repo.id).one(&self.db).await {
|
|
Ok(Some(r)) => r.project,
|
|
Ok(None) => Uuid::nil(),
|
|
Err(e) => {
|
|
slog::warn!(
|
|
self.logs,
|
|
"failed to look up project_id for activity log: {}",
|
|
e
|
|
);
|
|
Uuid::nil()
|
|
}
|
|
};
|
|
let user_uid = ctx.user().unwrap_or(Uuid::nil());
|
|
let _ = self
|
|
.project_log_activity(
|
|
project_id,
|
|
Some(repo.id),
|
|
user_uid,
|
|
super::super::project::activity::ActivityLogParams {
|
|
event_type: "branch_delete".to_string(),
|
|
title: format!("{} deleted branch '{}'", user_uid, name),
|
|
repo_id: Some(repo.id),
|
|
content: None,
|
|
event_id: None,
|
|
event_sub_id: None,
|
|
metadata: Some(serde_json::json!({"branch_name": name})),
|
|
is_private: false,
|
|
},
|
|
)
|
|
.await;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub async fn git_branch_delete_remote(
|
|
&self,
|
|
namespace: String,
|
|
repo_name: String,
|
|
name: String,
|
|
ctx: &Session,
|
|
) -> Result<(), AppError> {
|
|
let repo: repo::Model = self
|
|
.utils_check_repo_admin(namespace.clone(), repo_name.clone(), ctx)
|
|
.await?;
|
|
self.check_protection_for_deletion(repo.id, &name).await?;
|
|
let name_clone = name.clone();
|
|
|
|
git_spawn!(repo, domain -> {
|
|
domain.branch_delete_remote(&name_clone)
|
|
})?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub async fn git_branch_rename(
|
|
&self,
|
|
namespace: String,
|
|
repo_name: String,
|
|
request: BranchRenameRequest,
|
|
ctx: &Session,
|
|
) -> Result<BranchInfoResponse, AppError> {
|
|
let repo: repo::Model = self
|
|
.utils_check_repo_admin(namespace.clone(), repo_name.clone(), ctx)
|
|
.await?;
|
|
// Check: source branch cannot be deleted if protected
|
|
self.check_protection_for_deletion(repo.id, &request.old_name)
|
|
.await?;
|
|
// Check: target branch cannot be pushed if protected
|
|
self.check_protection_for_push(repo.id, &request.new_name)
|
|
.await?;
|
|
let old_name = request.old_name.clone();
|
|
let new_name = request.new_name.clone();
|
|
let old_name_for_spawn = old_name.clone();
|
|
let new_name_for_spawn = new_name.clone();
|
|
|
|
let info = git_spawn!(repo, domain -> {
|
|
domain.branch_rename(&old_name_for_spawn, &new_name_for_spawn)
|
|
})?;
|
|
|
|
let response = BranchInfoResponse::from(info);
|
|
let project_id = match repo_model::Entity::find_by_id(repo.id).one(&self.db).await {
|
|
Ok(Some(r)) => r.project,
|
|
Ok(None) => Uuid::nil(),
|
|
Err(e) => {
|
|
slog::warn!(
|
|
self.logs,
|
|
"failed to look up project_id for activity log: {}",
|
|
e
|
|
);
|
|
Uuid::nil()
|
|
}
|
|
};
|
|
let user_uid = ctx.user().unwrap_or(Uuid::nil());
|
|
let _ = self
|
|
.project_log_activity(
|
|
project_id,
|
|
Some(repo.id),
|
|
user_uid,
|
|
super::super::project::activity::ActivityLogParams {
|
|
event_type: "branch_rename".to_string(),
|
|
title: format!(
|
|
"{} renamed branch '{}' to '{}'",
|
|
user_uid, old_name, new_name
|
|
),
|
|
repo_id: Some(repo.id),
|
|
content: None,
|
|
event_id: None,
|
|
event_sub_id: None,
|
|
metadata: Some(serde_json::json!({"old_name": old_name, "new_name": new_name})),
|
|
is_private: false,
|
|
},
|
|
)
|
|
.await;
|
|
|
|
Ok(response)
|
|
}
|
|
|
|
pub async fn git_branch_move(
|
|
&self,
|
|
namespace: String,
|
|
repo_name: String,
|
|
request: BranchMoveRequest,
|
|
ctx: &Session,
|
|
) -> Result<BranchInfoResponse, AppError> {
|
|
let repo: repo::Model = self
|
|
.utils_check_repo_admin(namespace.clone(), repo_name.clone(), ctx)
|
|
.await?;
|
|
// Check: source branch cannot be deleted if protected
|
|
self.check_protection_for_deletion(repo.id, &request.name)
|
|
.await?;
|
|
// Check: target branch cannot be pushed if protected
|
|
self.check_protection_for_push(repo.id, &request.new_name)
|
|
.await?;
|
|
let name = request.name.clone();
|
|
let new_name = request.new_name.clone();
|
|
let force = request.force;
|
|
let name_for_spawn = name.clone();
|
|
let new_name_for_spawn = new_name.clone();
|
|
|
|
let info = git_spawn!(repo, domain -> {
|
|
domain.branch_move(&name_for_spawn, &new_name_for_spawn, force)
|
|
})?;
|
|
|
|
let response = BranchInfoResponse::from(info);
|
|
let project_id = match repo_model::Entity::find_by_id(repo.id).one(&self.db).await {
|
|
Ok(Some(r)) => r.project,
|
|
Ok(None) => Uuid::nil(),
|
|
Err(e) => {
|
|
slog::warn!(
|
|
self.logs,
|
|
"failed to look up project_id for activity log: {}",
|
|
e
|
|
);
|
|
Uuid::nil()
|
|
}
|
|
};
|
|
let user_uid = ctx.user().unwrap_or(Uuid::nil());
|
|
let _ = self
|
|
.project_log_activity(
|
|
project_id,
|
|
Some(repo.id),
|
|
user_uid,
|
|
super::super::project::activity::ActivityLogParams {
|
|
event_type: "branch_rename".to_string(),
|
|
title: format!("{} renamed branch '{}' to '{}'", user_uid, name, new_name),
|
|
repo_id: Some(repo.id),
|
|
content: None,
|
|
event_id: None,
|
|
event_sub_id: None,
|
|
metadata: Some(serde_json::json!({"old_name": name, "new_name": new_name})),
|
|
is_private: false,
|
|
},
|
|
)
|
|
.await;
|
|
|
|
Ok(response)
|
|
}
|
|
|
|
pub async fn git_branch_set_upstream(
|
|
&self,
|
|
namespace: String,
|
|
repo_name: String,
|
|
request: BranchSetUpstreamRequest,
|
|
ctx: &Session,
|
|
) -> Result<(), AppError> {
|
|
let repo: repo::Model = self
|
|
.utils_check_repo_admin(namespace, repo_name, ctx)
|
|
.await?;
|
|
let name = request.name.clone();
|
|
let upstream = request.upstream.clone();
|
|
|
|
git_spawn!(repo, domain -> {
|
|
domain.branch_set_upstream(&name, upstream.as_deref())
|
|
})?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub async fn git_branch_is_merged(
|
|
&self,
|
|
namespace: String,
|
|
repo_name: String,
|
|
query: BranchIsMergedQuery,
|
|
ctx: &Session,
|
|
) -> Result<BranchIsMergedResponse, AppError> {
|
|
let repo = self.utils_find_repo(namespace, repo_name, ctx).await?;
|
|
let branch = query.branch.clone();
|
|
let into = query.into.clone();
|
|
|
|
let is_merged = git_spawn!(repo, domain -> {
|
|
domain.branch_is_merged(&branch, &into)
|
|
})?;
|
|
|
|
Ok(BranchIsMergedResponse {
|
|
branch: query.branch,
|
|
into: query.into,
|
|
is_merged,
|
|
})
|
|
}
|
|
|
|
pub async fn git_branch_merge_base(
|
|
&self,
|
|
namespace: String,
|
|
repo_name: String,
|
|
query: BranchMergeBaseQuery,
|
|
ctx: &Session,
|
|
) -> Result<BranchMergeBaseResponse, AppError> {
|
|
let repo = self.utils_find_repo(namespace, repo_name, ctx).await?;
|
|
let branch1 = query.branch1.clone();
|
|
let branch2 = query.branch2.clone();
|
|
|
|
let base = git_spawn!(repo, domain -> {
|
|
domain.branch_merge_base(&branch1, &branch2)
|
|
})?;
|
|
|
|
Ok(BranchMergeBaseResponse {
|
|
branch1: query.branch1,
|
|
branch2: query.branch2,
|
|
base: base.to_string(),
|
|
})
|
|
}
|
|
|
|
pub async fn git_branch_is_ancestor(
|
|
&self,
|
|
namespace: String,
|
|
repo_name: String,
|
|
query: BranchIsAncestorQuery,
|
|
ctx: &Session,
|
|
) -> Result<BranchIsAncestorResponse, AppError> {
|
|
let repo = self.utils_find_repo(namespace, repo_name, ctx).await?;
|
|
let child = query.child.clone();
|
|
let ancestor = query.ancestor.clone();
|
|
|
|
let is_ancestor = git_spawn!(repo, domain -> {
|
|
domain.branch_is_ancestor(&child, &ancestor)
|
|
})?;
|
|
|
|
Ok(BranchIsAncestorResponse {
|
|
child: query.child,
|
|
ancestor: query.ancestor,
|
|
is_ancestor,
|
|
})
|
|
}
|
|
|
|
pub async fn git_branch_fast_forward(
|
|
&self,
|
|
namespace: String,
|
|
repo_name: String,
|
|
target: String,
|
|
force: Option<bool>,
|
|
ctx: &Session,
|
|
) -> Result<BranchFastForwardResponse, AppError> {
|
|
let repo = self.utils_find_repo(namespace, repo_name, ctx).await?;
|
|
let target_clone = target.clone();
|
|
|
|
let new_oid = git_spawn!(repo, domain -> {
|
|
domain.branch_fast_forward(&target_clone, force.unwrap_or(false))
|
|
})?;
|
|
|
|
Ok(BranchFastForwardResponse {
|
|
oid: new_oid.to_string(),
|
|
})
|
|
}
|
|
|
|
pub async fn git_branch_is_conflicted(
|
|
&self,
|
|
namespace: String,
|
|
repo_name: String,
|
|
ctx: &Session,
|
|
) -> Result<BranchIsConflictedResponse, AppError> {
|
|
let repo = self.utils_find_repo(namespace, repo_name, ctx).await?;
|
|
|
|
let is_conflicted = git_spawn!(repo, domain -> {
|
|
Ok::<_, git::GitError>(domain.branch_is_conflicted())
|
|
})?;
|
|
|
|
Ok(BranchIsConflictedResponse { is_conflicted })
|
|
}
|
|
}
|