a47ae83
use models::git::git_lfs_lock;
use sea_orm::{
ActiveModelTrait, ColumnTrait, EntityTrait, QueryFilter, QueryOrder, QuerySelect, Set,
};
use time::OffsetDateTime;
use uuid::Uuid;
use crate::context::facade::GitContext;
use crate::lfs::types::{LfsLock, LfsLockListResponse, LfsLockOwner, LfsVerifyLocksResponse};
use crate::lfs::validate::{canonical_oid, validate_lock_path};
use crate::service::GitService;
use crate::share::errors::GitError;
const DEFAULT_LOCK_LIMIT: u64 = 100;
const MAX_LOCK_LIMIT: u64 = 100;
pub async fn create(
svc: &GitService,
ctx: &GitContext,
caller: i64,
path: String,
oid: Option<String>,
) -> Result<LfsLock, GitError> {
if !ctx.model.enable_lfs {
return Err(GitError::NotFound);
}
let path = validate_lock_path(&path)?.to_string();
let oid = match oid {
Some(raw) => Some(canonical_oid(&raw)?),
None => None,
};
let now = OffsetDateTime::now_utc();
let row = git_lfs_lock::ActiveModel {
id: Set(Uuid::now_v7()),
repo_id: Set(ctx.model.id),
path: Set(path.clone()),
owner_id: Set(caller),
oid: Set(oid),
created_at: Set(now),
};
let inserted: git_lfs_lock::Model = row.insert(svc.db_write()).await?;
Ok(LfsLock {
id: inserted.id.to_string(),
path: inserted.path,
locked_at: format_time(inserted.created_at),
owner: LfsLockOwner {
name: owner_name(svc, caller).await,
},
})
}
pub async fn unlock(
svc: &GitService,
ctx: &GitContext,
caller: i64,
path: String,
force: bool,
) -> Result<(), GitError> {
let row = git_lfs_lock::Entity::find()
.filter(git_lfs_lock::Column::RepoId.eq(ctx.model.id))
.filter(git_lfs_lock::Column::Path.eq(&path))
.one(svc.db_write())
.await?
.ok_or(GitError::NotFound)?;
if row.owner_id != caller && !force {
return Err(GitError::Forbidden);
}
git_lfs_lock::Entity::delete_by_id(row.id)
.exec(svc.db_write())
.await?;
Ok(())
}
/// Unlock by lock id (the shape used by the LFS HTTP API:
/// `POST locks/{id}/unlock`). Same ownership/`force` rules as [`unlock`].
pub async fn unlock_by_id(
svc: &GitService,
ctx: &GitContext,
caller: i64,
lock_id: String,
force: bool,
) -> Result<(), GitError> {
let id = Uuid::parse_str(&lock_id).map_err(|_| GitError::NotFound)?;
let row = git_lfs_lock::Entity::find()
.filter(git_lfs_lock::Column::RepoId.eq(ctx.model.id))
.filter(git_lfs_lock::Column::Id.eq(id))
.one(svc.db_write())
.await?
.ok_or(GitError::NotFound)?;
if row.owner_id != caller && !force {
return Err(GitError::Forbidden);
}
git_lfs_lock::Entity::delete_by_id(row.id)
.exec(svc.db_write())
.await?;
Ok(())
}
/// List locks, oldest first, capped by `limit` (default/max 100). When the
/// page is full, `next_cursor` carries the last `locked_at` RFC3339 value so
/// the client can page forward.
pub async fn list(
svc: &GitService,
ctx: &GitContext,
cursor: Option<String>,
limit: u64,
) -> Result<LfsLockListResponse, GitError> {
let limit = normalize_limit(limit);
let mut find = git_lfs_lock::Entity::find()
.filter(git_lfs_lock::Column::RepoId.eq(ctx.model.id))
.order_by_asc(git_lfs_lock::Column::CreatedAt);
if let Some(cursor) = cursor {
let at = {
let cursor = &cursor;
OffsetDateTime::parse(cursor, &time::format_description::well_known::Rfc3339)
.map_err(|e| GitError::InvalidArgument(format!("invalid cursor: {e}")))
}?;
find = find.filter(git_lfs_lock::Column::CreatedAt.gt(at));
}
let mut rows = find.limit(limit + 1).all(svc.db_read()).await?;
let has_more = rows.len() as u64 > limit;
rows.truncate(limit as usize);
let locks = to_locks(svc, rows).await;
let next_cursor = if has_more {
locks.last().map(|l| l.locked_at.clone())
} else {
None
};
Ok(LfsLockListResponse { locks, next_cursor })
}
/// Split this repo's locks into `ours` (locked by the caller) and `theirs`.
pub async fn verify_locks(
svc: &GitService,
ctx: &GitContext,
caller: i64,
limit: u64,
) -> Result<LfsVerifyLocksResponse, GitError> {
let rows = git_lfs_lock::Entity::find()
.filter(git_lfs_lock::Column::RepoId.eq(ctx.model.id))
.order_by_asc(git_lfs_lock::Column::CreatedAt)
.limit(normalize_limit(limit))
.all(svc.db_read())
.await?;
let mut ours = Vec::new();
let mut theirs = Vec::new();
for row in rows {
let lock = LfsLock {
id: row.id.to_string(),
path: row.path,
locked_at: format_time(row.created_at),
owner: LfsLockOwner {
name: owner_name(svc, row.owner_id).await,
},
};
if row.owner_id == caller {
ours.push(lock);
} else {
theirs.push(lock);
}
}
Ok(LfsVerifyLocksResponse {
ours,
theirs,
next_cursor: None,
})
}
async fn to_locks(svc: &GitService, rows: Vec<git_lfs_lock::Model>) -> Vec<LfsLock> {
let mut locks = Vec::with_capacity(rows.len());
for row in rows {
locks.push(LfsLock {
id: row.id.to_string(),
path: row.path,
locked_at: format_time(row.created_at),
owner: LfsLockOwner {
name: owner_name(svc, row.owner_id).await,
},
});
}
locks
}
async fn owner_name(svc: &GitService, uid: i64) -> String {
match auth::users::get_public_user(&svc.accounts, uid).await {
Ok(Some(user)) if !user.displayname.trim().is_empty() => user.displayname,
Ok(Some(user)) => user.username,
_ => uid.to_string(),
}
}
fn format_time(at: OffsetDateTime) -> String {
at.format(&time::format_description::well_known::Rfc3339)
.unwrap_or_default()
}
fn normalize_limit(limit: u64) -> u64 {
match limit {
0 => DEFAULT_LOCK_LIMIT,
n if n > MAX_LOCK_LIMIT => MAX_LOCK_LIMIT,
n => n,
}
}