max / makenotwork
8 files changed,
+122 insertions,
-81 deletions
| @@ -276,6 +276,25 @@ impl FromRef<AppState> for Sync { | |||
| 276 | 276 | } | |
| 277 | 277 | } | |
| 278 | 278 | ||
| 279 | + | /// Git source-browser slice: the syntax highlighter (blob rendering) plus the | |
| 280 | + | /// semaphore that bounds concurrent smart-HTTP clone/fetch child processes. | |
| 281 | + | /// Draws `syntax` from the top level and `git_smart_http_semaphore` from | |
| 282 | + | /// [`AppLimiters`]. | |
| 283 | + | #[derive(Clone)] | |
| 284 | + | pub struct Git { | |
| 285 | + | pub syntax: Option<Arc<git::SyntaxHighlighter>>, | |
| 286 | + | pub smart_http_semaphore: Arc<tokio::sync::Semaphore>, | |
| 287 | + | } | |
| 288 | + | ||
| 289 | + | impl FromRef<AppState> for Git { | |
| 290 | + | fn from_ref(s: &AppState) -> Self { | |
| 291 | + | Self { | |
| 292 | + | syntax: s.syntax.clone(), | |
| 293 | + | smart_http_semaphore: s.limiters.git_smart_http_semaphore.clone(), | |
| 294 | + | } | |
| 295 | + | } | |
| 296 | + | } | |
| 297 | + | ||
| 279 | 298 | /// Observability/lifecycle slice: uptime clocks, the pending-restart flag, and | |
| 280 | 299 | /// the Prometheus render handle. Used by status/health/admin/metrics handlers. | |
| 281 | 300 | /// (`started_at`/`start_instant` are `#[from_ref(skip)]` on `AppState`, so this | |
| @@ -716,6 +735,7 @@ mod from_ref_slices { | |||
| 716 | 735 | _assert_from_ref::<Integrations>(); | |
| 717 | 736 | _assert_from_ref::<Scanning>(); | |
| 718 | 737 | _assert_from_ref::<Sync>(); | |
| 738 | + | _assert_from_ref::<Git>(); | |
| 719 | 739 | _assert_from_ref::<Ops>(); | |
| 720 | 740 | } | |
| 721 | 741 | } |
| @@ -8,15 +8,18 @@ use axum::{ | |||
| 8 | 8 | use serde::Deserialize; | |
| 9 | 9 | use tower_sessions::Session; | |
| 10 | 10 | ||
| 11 | + | use sqlx::PgPool; | |
| 12 | + | ||
| 11 | 13 | use crate::{ | |
| 12 | 14 | auth::MaybeUserVerified, | |
| 15 | + | config::Config, | |
| 13 | 16 | constants, | |
| 14 | 17 | db, | |
| 15 | 18 | error::{AppError, Result}, | |
| 16 | 19 | git, | |
| 17 | 20 | helpers::get_csrf_token, | |
| 18 | 21 | templates::*, | |
| 19 | - | AppState, | |
| 22 | + | Git, | |
| 20 | 23 | }; | |
| 21 | 24 | ||
| 22 | 25 | use super::{ | |
| @@ -26,12 +29,13 @@ use super::{ | |||
| 26 | 29 | /// `GET /git/{owner}/{repo}`: repo overview (tree at HEAD + README). | |
| 27 | 30 | #[tracing::instrument(skip_all, name = "git::repo_overview")] | |
| 28 | 31 | pub(super) async fn repo_overview( | |
| 29 | - | State(state): State<AppState>, | |
| 32 | + | State(db): State<PgPool>, | |
| 33 | + | State(config): State<Config>, | |
| 30 | 34 | session: Session, | |
| 31 | 35 | MaybeUserVerified(maybe_user): MaybeUserVerified, | |
| 32 | 36 | Path((owner, repo_name)): Path<(String, String)>, | |
| 33 | 37 | ) -> Result<impl IntoResponse> { | |
| 34 | - | let resolved = resolve_repo(&state, &owner, &repo_name, maybe_user.as_ref().map(|u| u.id)).await?; | |
| 38 | + | let resolved = resolve_repo(&db, &config,&owner, &repo_name, maybe_user.as_ref().map(|u| u.id)).await?; | |
| 35 | 39 | let repo_name_c = repo_name.clone(); | |
| 36 | 40 | let (info, refs, tree_items, readme_html) = resolved.with_repo(move |repo| { | |
| 37 | 41 | let info = git::repo_info(repo, &repo_name_c); | |
| @@ -45,8 +49,8 @@ pub(super) async fn repo_overview( | |||
| 45 | 49 | let csrf_token = get_csrf_token(&session).await; | |
| 46 | 50 | ||
| 47 | 51 | let is_owner = maybe_user.as_ref().map(|u| u.id) == Some(resolved.db_user.id); | |
| 48 | - | let (linked_project, release_items) = fetch_linked_releases(&state, &resolved.db_repo).await; | |
| 49 | - | let (open_issue_count, _) = db::issues::get_issue_counts(&state.db, resolved.db_repo.id).await.unwrap_or((0, 0)); | |
| 52 | + | let (linked_project, release_items) = fetch_linked_releases(&db,&resolved.db_repo).await; | |
| 53 | + | let (open_issue_count, _) = db::issues::get_issue_counts(&db, resolved.db_repo.id).await.unwrap_or((0, 0)); | |
| 50 | 54 | ||
| 51 | 55 | let description = if resolved.db_repo.description.is_empty() { | |
| 52 | 56 | info.description | |
| @@ -64,8 +68,8 @@ pub(super) async fn repo_overview( | |||
| 64 | 68 | refs, | |
| 65 | 69 | tree_items, | |
| 66 | 70 | readme_html, | |
| 67 | - | host_url: state.config.host_url.clone(), | |
| 68 | - | git_ssh_host: state.config.build.git_ssh_host.clone(), | |
| 71 | + | host_url: config.host_url.clone(), | |
| 72 | + | git_ssh_host: config.build.git_ssh_host.clone(), | |
| 69 | 73 | linked_project, | |
| 70 | 74 | release_items, | |
| 71 | 75 | open_issue_count, | |
| @@ -77,12 +81,13 @@ pub(super) async fn repo_overview( | |||
| 77 | 81 | /// `GET /git/{owner}/{repo}/tree/{ref}`: tree root at a specific ref. | |
| 78 | 82 | #[tracing::instrument(skip_all, name = "git::tree_root")] | |
| 79 | 83 | pub(super) async fn tree_root( | |
| 80 | - | State(state): State<AppState>, | |
| 84 | + | State(db): State<PgPool>, | |
| 85 | + | State(config): State<Config>, | |
| 81 | 86 | session: Session, | |
| 82 | 87 | MaybeUserVerified(maybe_user): MaybeUserVerified, | |
| 83 | 88 | Path((owner, repo_name, git_ref)): Path<(String, String, String)>, | |
| 84 | 89 | ) -> Result<impl IntoResponse> { | |
| 85 | - | let resolved = resolve_repo(&state, &owner, &repo_name, maybe_user.as_ref().map(|u| u.id)).await?; | |
| 90 | + | let resolved = resolve_repo(&db, &config,&owner, &repo_name, maybe_user.as_ref().map(|u| u.id)).await?; | |
| 86 | 91 | let git_ref_c = git_ref.clone(); | |
| 87 | 92 | let (refs, tree_items, readme_html) = resolved.with_repo(move |repo| { | |
| 88 | 93 | let refs = git::list_refs(repo); | |
| @@ -95,8 +100,8 @@ pub(super) async fn tree_root( | |||
| 95 | 100 | let csrf_token = get_csrf_token(&session).await; | |
| 96 | 101 | let is_owner = maybe_user.as_ref().map(|u| u.id) == Some(resolved.db_user.id); | |
| 97 | 102 | ||
| 98 | - | let (linked_project, release_items) = fetch_linked_releases(&state, &resolved.db_repo).await; | |
| 99 | - | let (open_issue_count, _) = db::issues::get_issue_counts(&state.db, resolved.db_repo.id).await.unwrap_or((0, 0)); | |
| 103 | + | let (linked_project, release_items) = fetch_linked_releases(&db,&resolved.db_repo).await; | |
| 104 | + | let (open_issue_count, _) = db::issues::get_issue_counts(&db, resolved.db_repo.id).await.unwrap_or((0, 0)); | |
| 100 | 105 | ||
| 101 | 106 | let description = if resolved.db_repo.description.is_empty() { | |
| 102 | 107 | None | |
| @@ -114,8 +119,8 @@ pub(super) async fn tree_root( | |||
| 114 | 119 | refs, | |
| 115 | 120 | tree_items, | |
| 116 | 121 | readme_html, | |
| 117 | - | host_url: state.config.host_url.clone(), | |
| 118 | - | git_ssh_host: state.config.build.git_ssh_host.clone(), | |
| 122 | + | host_url: config.host_url.clone(), | |
| 123 | + | git_ssh_host: config.build.git_ssh_host.clone(), | |
| 119 | 124 | linked_project, | |
| 120 | 125 | release_items, | |
| 121 | 126 | open_issue_count, | |
| @@ -127,14 +132,16 @@ pub(super) async fn tree_root( | |||
| 127 | 132 | /// `GET /git/{owner}/{repo}/tree/{ref}/{*path}`: file or subdirectory. | |
| 128 | 133 | #[tracing::instrument(skip_all, name = "git::tree_or_file")] | |
| 129 | 134 | pub(super) async fn tree_or_file( | |
| 130 | - | State(state): State<AppState>, | |
| 135 | + | State(db): State<PgPool>, | |
| 136 | + | State(config): State<Config>, | |
| 137 | + | State(git): State<Git>, | |
| 131 | 138 | session: Session, | |
| 132 | 139 | MaybeUserVerified(maybe_user): MaybeUserVerified, | |
| 133 | 140 | Path((owner, repo_name, git_ref, path)): Path<(String, String, String, String)>, | |
| 134 | 141 | ) -> Result<Response> { | |
| 135 | - | let resolved = resolve_repo(&state, &owner, &repo_name, maybe_user.as_ref().map(|u| u.id)).await?; | |
| 142 | + | let resolved = resolve_repo(&db, &config,&owner, &repo_name, maybe_user.as_ref().map(|u| u.id)).await?; | |
| 136 | 143 | let is_owner = maybe_user.as_ref().map(|u| u.id) == Some(resolved.db_user.id); | |
| 137 | - | let (open_issue_count, _) = db::issues::get_issue_counts(&state.db, resolved.db_repo.id).await.unwrap_or((0, 0)); | |
| 144 | + | let (open_issue_count, _) = db::issues::get_issue_counts(&db, resolved.db_repo.id).await.unwrap_or((0, 0)); | |
| 138 | 145 | ||
| 139 | 146 | // Resolve the ref and read the path as a directory (preferred) or file in | |
| 140 | 147 | // one blocking-pool pass. | |
| @@ -195,7 +202,7 @@ pub(super) async fn tree_or_file( | |||
| 195 | 202 | ||
| 196 | 203 | let highlighted_lines = if file_content.is_binary { | |
| 197 | 204 | Vec::new() | |
| 198 | - | } else if let Some(ref highlighter) = state.syntax { | |
| 205 | + | } else if let Some(ref highlighter) = git.syntax { | |
| 199 | 206 | let highlighted = highlighter.highlight(&file_content.content, &filename); | |
| 200 | 207 | highlighted.lines().map(String::from).collect() | |
| 201 | 208 | } else { | |
| @@ -243,13 +250,14 @@ pub(super) struct CommitQuery { | |||
| 243 | 250 | /// `GET /git/{owner}/{repo}/commits/{ref}`: commit log with pagination. | |
| 244 | 251 | #[tracing::instrument(skip_all, name = "git::commit_log")] | |
| 245 | 252 | pub(super) async fn commit_log( | |
| 246 | - | State(state): State<AppState>, | |
| 253 | + | State(db): State<PgPool>, | |
| 254 | + | State(config): State<Config>, | |
| 247 | 255 | session: Session, | |
| 248 | 256 | MaybeUserVerified(maybe_user): MaybeUserVerified, | |
| 249 | 257 | Path((owner, repo_name, git_ref)): Path<(String, String, String)>, | |
| 250 | 258 | Query(query): Query<CommitQuery>, | |
| 251 | 259 | ) -> Result<impl IntoResponse> { | |
| 252 | - | let resolved = resolve_repo(&state, &owner, &repo_name, maybe_user.as_ref().map(|u| u.id)).await?; | |
| 260 | + | let resolved = resolve_repo(&db, &config,&owner, &repo_name, maybe_user.as_ref().map(|u| u.id)).await?; | |
| 253 | 261 | ||
| 254 | 262 | let page = query.page.unwrap_or(1).clamp(1, 10_000); | |
| 255 | 263 | let limit = constants::GIT_COMMITS_PER_PAGE; | |
| @@ -268,7 +276,7 @@ pub(super) async fn commit_log( | |||
| 268 | 276 | let csrf_token = get_csrf_token(&session).await; | |
| 269 | 277 | let is_owner = maybe_user.as_ref().map(|u| u.id) == Some(resolved.db_user.id); | |
| 270 | 278 | ||
| 271 | - | let (open_issue_count, _) = db::issues::get_issue_counts(&state.db, resolved.db_repo.id).await.unwrap_or((0, 0)); | |
| 279 | + | let (open_issue_count, _) = db::issues::get_issue_counts(&db, resolved.db_repo.id).await.unwrap_or((0, 0)); | |
| 272 | 280 | ||
| 273 | 281 | Ok(GitCommitsTemplate { | |
| 274 | 282 | csrf_token, | |
| @@ -289,12 +297,13 @@ pub(super) async fn commit_log( | |||
| 289 | 297 | /// `GET /git/{owner}/{repo}/commit/{oid}`: commit detail with inline diffs. | |
| 290 | 298 | #[tracing::instrument(skip_all, name = "git::commit_detail_page")] | |
| 291 | 299 | pub(super) async fn commit_detail_page( | |
| 292 | - | State(state): State<AppState>, | |
| 300 | + | State(db): State<PgPool>, | |
| 301 | + | State(config): State<Config>, | |
| 293 | 302 | session: Session, | |
| 294 | 303 | MaybeUserVerified(maybe_user): MaybeUserVerified, | |
| 295 | 304 | Path((owner, repo_name, oid_str)): Path<(String, String, String)>, | |
| 296 | 305 | ) -> Result<impl IntoResponse> { | |
| 297 | - | let resolved = resolve_repo(&state, &owner, &repo_name, maybe_user.as_ref().map(|u| u.id)).await?; | |
| 306 | + | let resolved = resolve_repo(&db, &config,&owner, &repo_name, maybe_user.as_ref().map(|u| u.id)).await?; | |
| 298 | 307 | ||
| 299 | 308 | let oid = git2::Oid::from_str(&oid_str).map_err(|_| AppError::NotFound)?; | |
| 300 | 309 | let repo_name_c = repo_name.clone(); | |
| @@ -318,7 +327,7 @@ pub(super) async fn commit_detail_page( | |||
| 318 | 327 | ||
| 319 | 328 | let csrf_token = get_csrf_token(&session).await; | |
| 320 | 329 | let is_owner = maybe_user.as_ref().map(|u| u.id) == Some(resolved.db_user.id); | |
| 321 | - | let (open_issue_count, _) = db::issues::get_issue_counts(&state.db, resolved.db_repo.id).await.unwrap_or((0, 0)); | |
| 330 | + | let (open_issue_count, _) = db::issues::get_issue_counts(&db, resolved.db_repo.id).await.unwrap_or((0, 0)); | |
| 322 | 331 | ||
| 323 | 332 | Ok(GitCommitDetailTemplate { | |
| 324 | 333 | csrf_token, | |
| @@ -341,12 +350,13 @@ pub(super) async fn commit_detail_page( | |||
| 341 | 350 | /// `GET /git/{owner}/{repo}/blame/{ref}/{*path}`: blame view. | |
| 342 | 351 | #[tracing::instrument(skip_all, name = "git::blame_view")] | |
| 343 | 352 | pub(super) async fn blame_view( | |
| 344 | - | State(state): State<AppState>, | |
| 353 | + | State(db): State<PgPool>, | |
| 354 | + | State(config): State<Config>, | |
| 345 | 355 | session: Session, | |
| 346 | 356 | MaybeUserVerified(maybe_user): MaybeUserVerified, | |
| 347 | 357 | Path((owner, repo_name, git_ref, path)): Path<(String, String, String, String)>, | |
| 348 | 358 | ) -> Result<impl IntoResponse> { | |
| 349 | - | let resolved = resolve_repo(&state, &owner, &repo_name, maybe_user.as_ref().map(|u| u.id)).await?; | |
| 359 | + | let resolved = resolve_repo(&db, &config,&owner, &repo_name, maybe_user.as_ref().map(|u| u.id)).await?; | |
| 350 | 360 | let git_ref_c = git_ref.clone(); | |
| 351 | 361 | let path_c = path.clone(); | |
| 352 | 362 | let (blame_lines, refs) = resolved.with_repo(move |repo| { | |
| @@ -361,7 +371,7 @@ pub(super) async fn blame_view( | |||
| 361 | 371 | ||
| 362 | 372 | let csrf_token = get_csrf_token(&session).await; | |
| 363 | 373 | let is_owner = maybe_user.as_ref().map(|u| u.id) == Some(resolved.db_user.id); | |
| 364 | - | let (open_issue_count, _) = db::issues::get_issue_counts(&state.db, resolved.db_repo.id).await.unwrap_or((0, 0)); | |
| 374 | + | let (open_issue_count, _) = db::issues::get_issue_counts(&db, resolved.db_repo.id).await.unwrap_or((0, 0)); | |
| 365 | 375 | ||
| 366 | 376 | Ok(GitBlameTemplate { | |
| 367 | 377 | csrf_token, | |
| @@ -383,22 +393,22 @@ pub(super) async fn blame_view( | |||
| 383 | 393 | /// `GET /git/{owner}`: user's repository listing. | |
| 384 | 394 | #[tracing::instrument(skip_all, name = "git::user_repos")] | |
| 385 | 395 | pub(super) async fn user_repos( | |
| 386 | - | State(state): State<AppState>, | |
| 396 | + | State(db): State<PgPool>, | |
| 387 | 397 | session: Session, | |
| 388 | 398 | MaybeUserVerified(maybe_user): MaybeUserVerified, | |
| 389 | 399 | Path(owner): Path<String>, | |
| 390 | 400 | ) -> Result<impl IntoResponse> { | |
| 391 | 401 | let username = db::Username::new(&owner).map_err(|_| AppError::NotFound)?; | |
| 392 | - | let db_user = db::users::get_user_by_username(&state.db, &username) | |
| 402 | + | let db_user = db::users::get_user_by_username(&db, &username) | |
| 393 | 403 | .await? | |
| 394 | 404 | .ok_or(AppError::NotFound)?; | |
| 395 | 405 | ||
| 396 | 406 | let is_owner = maybe_user.as_ref().map(|u| u.id) == Some(db_user.id); | |
| 397 | 407 | ||
| 398 | 408 | let repos = if is_owner { | |
| 399 | - | db::git_repos::get_repos_by_user(&state.db, db_user.id).await? | |
| 409 | + | db::git_repos::get_repos_by_user(&db, db_user.id).await? | |
| 400 | 410 | } else { | |
| 401 | - | db::git_repos::get_public_repos_by_user(&state.db, db_user.id).await? | |
| 411 | + | db::git_repos::get_public_repos_by_user(&db, db_user.id).await? | |
| 402 | 412 | }; | |
| 403 | 413 | ||
| 404 | 414 | let csrf_token = get_csrf_token(&session).await; | |
| @@ -421,7 +431,7 @@ pub(super) struct ExploreQuery { | |||
| 421 | 431 | /// `GET /git`: public explore page listing all public repos. | |
| 422 | 432 | #[tracing::instrument(skip_all, name = "git::git_explore")] | |
| 423 | 433 | pub(super) async fn git_landing( | |
| 424 | - | State(state): State<AppState>, | |
| 434 | + | State(db): State<PgPool>, | |
| 425 | 435 | session: Session, | |
| 426 | 436 | MaybeUserVerified(maybe_user): MaybeUserVerified, | |
| 427 | 437 | Query(query): Query<ExploreQuery>, | |
| @@ -430,10 +440,10 @@ pub(super) async fn git_landing( | |||
| 430 | 440 | let limit = constants::GIT_REPOS_PER_PAGE; | |
| 431 | 441 | let offset = (page - 1).saturating_mul(limit); | |
| 432 | 442 | ||
| 433 | - | let repos = db::git_repos::get_all_public_repos(&state.db, (limit + 1) as i64, offset as i64).await?; | |
| 443 | + | let repos = db::git_repos::get_all_public_repos(&db, (limit + 1) as i64, offset as i64).await?; | |
| 434 | 444 | let has_more = repos.len() > limit; | |
| 435 | 445 | let repos: Vec<_> = repos.into_iter().take(limit).collect(); | |
| 436 | - | let total_count = db::git_repos::count_all_public_repos(&state.db).await?; | |
| 446 | + | let total_count = db::git_repos::count_all_public_repos(&db).await?; | |
| 437 | 447 | ||
| 438 | 448 | let csrf_token = get_csrf_token(&session).await; | |
| 439 | 449 | ||
| @@ -450,13 +460,14 @@ pub(super) async fn git_landing( | |||
| 450 | 460 | /// `GET /git/{owner}/{repo}/log/{ref}/{*path}`: per-file commit history. | |
| 451 | 461 | #[tracing::instrument(skip_all, name = "git::file_log")] | |
| 452 | 462 | pub(super) async fn file_log( | |
| 453 | - | State(state): State<AppState>, | |
| 463 | + | State(db): State<PgPool>, | |
| 464 | + | State(config): State<Config>, | |
| 454 | 465 | session: Session, | |
| 455 | 466 | MaybeUserVerified(maybe_user): MaybeUserVerified, | |
| 456 | 467 | Path((owner, repo_name, git_ref, path)): Path<(String, String, String, String)>, | |
| 457 | 468 | Query(query): Query<CommitQuery>, | |
| 458 | 469 | ) -> Result<impl IntoResponse> { | |
| 459 | - | let resolved = resolve_repo(&state, &owner, &repo_name, maybe_user.as_ref().map(|u| u.id)).await?; | |
| 470 | + | let resolved = resolve_repo(&db, &config,&owner, &repo_name, maybe_user.as_ref().map(|u| u.id)).await?; | |
| 460 | 471 | ||
| 461 | 472 | let page = query.page.unwrap_or(1).clamp(1, 10_000); | |
| 462 | 473 | let limit = constants::GIT_COMMITS_PER_PAGE; | |
| @@ -484,7 +495,7 @@ pub(super) async fn file_log( | |||
| 484 | 495 | let breadcrumbs = build_breadcrumbs(&path); | |
| 485 | 496 | let csrf_token = get_csrf_token(&session).await; | |
| 486 | 497 | let is_owner = maybe_user.as_ref().map(|u| u.id) == Some(resolved.db_user.id); | |
| 487 | - | let (open_issue_count, _) = db::issues::get_issue_counts(&state.db, resolved.db_repo.id).await.unwrap_or((0, 0)); | |
| 498 | + | let (open_issue_count, _) = db::issues::get_issue_counts(&db, resolved.db_repo.id).await.unwrap_or((0, 0)); | |
| 488 | 499 | ||
| 489 | 500 | Ok(GitFileLogTemplate { | |
| 490 | 501 | csrf_token, |
| @@ -12,7 +12,10 @@ use axum::{ | |||
| 12 | 12 | }; | |
| 13 | 13 | use tower_governor::GovernorLayer; | |
| 14 | 14 | ||
| 15 | + | use sqlx::PgPool; | |
| 16 | + | ||
| 15 | 17 | use crate::{ | |
| 18 | + | config::Config, | |
| 16 | 19 | constants, | |
| 17 | 20 | db::{self, DbGitRepo, DbUser, UserId, Username}, | |
| 18 | 21 | error::{AppError, Result}, | |
| @@ -78,9 +81,8 @@ pub fn format_size(bytes: &u64) -> String { | |||
| 78 | 81 | use crate::helpers::escape_html; | |
| 79 | 82 | ||
| 80 | 83 | /// Get the repos root path from config, or return 404 if git is not configured. | |
| 81 | - | pub(crate) fn repos_root(state: &AppState) -> Result<std::path::PathBuf> { | |
| 82 | - | state | |
| 83 | - | .config | |
| 84 | + | pub(crate) fn repos_root(config: &Config) -> Result<std::path::PathBuf> { | |
| 85 | + | config | |
| 84 | 86 | .build.git_repos_path | |
| 85 | 87 | .as_ref() | |
| 86 | 88 | .map(std::path::PathBuf::from) | |
| @@ -161,21 +163,22 @@ fn resolve_repo_name(repo_name: &str) -> &str { | |||
| 161 | 163 | ||
| 162 | 164 | #[tracing::instrument(skip_all, name = "git::resolve_repo", fields(owner = %owner, repo = %repo_name))] | |
| 163 | 165 | pub(crate) async fn resolve_repo( | |
| 164 | - | state: &AppState, | |
| 166 | + | db: &PgPool, | |
| 167 | + | config: &Config, | |
| 165 | 168 | owner: &str, | |
| 166 | 169 | repo_name: &str, | |
| 167 | 170 | session_user_id: Option<UserId>, | |
| 168 | 171 | ) -> Result<ResolvedRepo> { | |
| 169 | - | let root = repos_root(state)?; | |
| 172 | + | let root = repos_root(config)?; | |
| 170 | 173 | ||
| 171 | 174 | // 1. Look up user by URL owner | |
| 172 | 175 | let username = Username::new(owner).map_err(|_| AppError::NotFound)?; | |
| 173 | - | let db_user = db::users::get_user_by_username(&state.db, &username) | |
| 176 | + | let db_user = db::users::get_user_by_username(db, &username) | |
| 174 | 177 | .await? | |
| 175 | 178 | .ok_or(AppError::NotFound)?; | |
| 176 | 179 | ||
| 177 | 180 | // 2. Look up repo in DB | |
| 178 | - | let db_repo = match db::git_repos::get_repo_by_user_and_name(&state.db, db_user.id, repo_name).await? { | |
| 181 | + | let db_repo = match db::git_repos::get_repo_by_user_and_name(db, db_user.id, repo_name).await? { | |
| 179 | 182 | Some(r) => r, | |
| 180 | 183 | None => { | |
| 181 | 184 | // Auto-register: repo exists on disk but not in DB. Only the owner | |
| @@ -191,11 +194,11 @@ pub(crate) async fn resolve_repo( | |||
| 191 | 194 | if git::open_repo(&root, disk_dir, repo_name).is_err() { | |
| 192 | 195 | return Err(AppError::NotFound); | |
| 193 | 196 | } | |
| 194 | - | match db::git_repos::create_repo(&state.db, db_user.id, repo_name).await { | |
| 197 | + | match db::git_repos::create_repo(db, db_user.id, repo_name).await { | |
| 195 | 198 | Ok(r) => r, | |
| 196 | 199 | Err(e) => { | |
| 197 | 200 | tracing::debug!(owner, repo_name, error = ?e, "auto-register failed, retrying lookup"); | |
| 198 | - | db::git_repos::get_repo_by_user_and_name(&state.db, db_user.id, repo_name) | |
| 201 | + | db::git_repos::get_repo_by_user_and_name(db, db_user.id, repo_name) | |
| 199 | 202 | .await? | |
| 200 | 203 | .ok_or(AppError::NotFound)? | |
| 201 | 204 | } | |
| @@ -211,7 +214,7 @@ pub(crate) async fn resolve_repo( | |||
| 211 | 214 | let is_owner = session_user_id == Some(db_user.id); | |
| 212 | 215 | if db_repo.visibility == db::Visibility::Private && !is_owner { | |
| 213 | 216 | let is_collaborator = match session_user_id { | |
| 214 | - | Some(uid) => db::repo_collaborators::is_collaborator(&state.db, db_repo.id, uid).await?, | |
| 217 | + | Some(uid) => db::repo_collaborators::is_collaborator(db, db_repo.id, uid).await?, | |
| 215 | 218 | None => false, | |
| 216 | 219 | }; | |
| 217 | 220 | if !is_collaborator { | |
| @@ -245,7 +248,7 @@ pub(crate) struct GitHttpPrincipal { | |||
| 245 | 248 | /// or expired credentials — callers treat that as unauthenticated, and a | |
| 246 | 249 | /// private repo then 404s exactly as it does for an anonymous browser request. | |
| 247 | 250 | pub(crate) async fn resolve_git_http_principal( | |
| 248 | - | state: &AppState, | |
| 251 | + | db: &PgPool, | |
| 249 | 252 | headers: &axum::http::HeaderMap, | |
| 250 | 253 | session_user_id: Option<UserId>, | |
| 251 | 254 | ) -> Option<GitHttpPrincipal> { | |
| @@ -255,7 +258,7 @@ pub(crate) async fn resolve_git_http_principal( | |||
| 255 | 258 | let header = headers.get(axum::http::header::AUTHORIZATION)?.to_str().ok()?; | |
| 256 | 259 | let token = parse_basic_auth_token(header)?; | |
| 257 | 260 | let hash = crate::crypto::git_token_hash(&token); | |
| 258 | - | let resolved = db::git_access_tokens::resolve_active(&state.db, &hash).await.ok()??; | |
| 261 | + | let resolved = db::git_access_tokens::resolve_active(db, &hash).await.ok()??; | |
| 259 | 262 | Some(GitHttpPrincipal { user_id: resolved.user_id, token_push: Some(resolved.can_push) }) | |
| 260 | 263 | } | |
| 261 | 264 | ||
| @@ -280,7 +283,7 @@ fn parse_basic_auth_token(header: &str) -> Option<String> { | |||
| 280 | 283 | /// Look up the project linked to a git repo and fetch its public items with versions. | |
| 281 | 284 | #[tracing::instrument(skip_all, name = "git::fetch_linked_releases", fields(project_id = tracing::field::Empty))] | |
| 282 | 285 | async fn fetch_linked_releases( | |
| 283 | - | state: &AppState, | |
| 286 | + | db: &PgPool, | |
| 284 | 287 | db_repo: &DbGitRepo, | |
| 285 | 288 | ) -> (Option<Project>, Vec<ReleaseItem>) { | |
| 286 | 289 | let project_id = match db_repo.project_id { | |
| @@ -289,7 +292,7 @@ async fn fetch_linked_releases( | |||
| 289 | 292 | }; | |
| 290 | 293 | tracing::Span::current().record("project_id", tracing::field::display(project_id)); | |
| 291 | 294 | ||
| 292 | - | let db_project = match db::projects::get_project_by_id(&state.db, project_id).await { | |
| 295 | + | let db_project = match db::projects::get_project_by_id(db, project_id).await { | |
| 293 | 296 | Ok(Some(p)) if p.is_public => p, | |
| 294 | 297 | Ok(_) => return (None, Vec::new()), | |
| 295 | 298 | Err(e) => { | |
| @@ -298,7 +301,7 @@ async fn fetch_linked_releases( | |||
| 298 | 301 | } | |
| 299 | 302 | }; | |
| 300 | 303 | ||
| 301 | - | let db_items = match db::items::get_public_items_by_project(&state.db, db_project.id).await { | |
| 304 | + | let db_items = match db::items::get_public_items_by_project(db, db_project.id).await { | |
| 302 | 305 | Ok(items) => items, | |
| 303 | 306 | Err(e) => { | |
| 304 | 307 | tracing::warn!(%project_id, error = %e, "linked project items lookup failed; rendering project without releases"); | |
| @@ -309,13 +312,13 @@ async fn fetch_linked_releases( | |||
| 309 | 312 | // Batch-load versions and tags for all items in two queries rather than | |
| 310 | 313 | // two per item (the previous N+1). Both helpers return maps keyed by item. | |
| 311 | 314 | let item_ids: Vec<_> = db_items.iter().map(|i| i.id).collect(); | |
| 312 | - | let mut versions_by_item = db::versions::get_versions_by_items(&state.db, &item_ids) | |
| 315 | + | let mut versions_by_item = db::versions::get_versions_by_items(db, &item_ids) | |
| 313 | 316 | .await | |
| 314 | 317 | .unwrap_or_else(|e| { | |
| 315 | 318 | tracing::warn!(%project_id, error = %e, "batch version load failed; releases will omit versions"); | |
| 316 | 319 | Default::default() | |
| 317 | 320 | }); | |
| 318 | - | let tags_by_item = db::tags::get_tags_for_items(&state.db, &item_ids) | |
| 321 | + | let tags_by_item = db::tags::get_tags_for_items(db, &item_ids) | |
| 319 | 322 | .await | |
| 320 | 323 | .unwrap_or_else(|e| { | |
| 321 | 324 | tracing::warn!(%project_id, error = %e, "batch tag load failed; releases will omit tags"); |
| @@ -9,12 +9,15 @@ use axum::{ | |||
| 9 | 9 | use serde::Deserialize; | |
| 10 | 10 | use tokio_util::io::ReaderStream; | |
| 11 | 11 | ||
| 12 | + | use sqlx::PgPool; | |
| 13 | + | ||
| 12 | 14 | use crate::{ | |
| 13 | 15 | auth::MaybeUserVerified, | |
| 16 | + | config::Config, | |
| 14 | 17 | constants, | |
| 15 | 18 | error::{AppError, Result, ResultExt}, | |
| 16 | 19 | git, | |
| 17 | - | AppState, | |
| 20 | + | Git, | |
| 18 | 21 | }; | |
| 19 | 22 | ||
| 20 | 23 | use super::{repos_root, resolve_repo, resolve_repo_name}; | |
| @@ -22,11 +25,12 @@ use super::{repos_root, resolve_repo, resolve_repo_name}; | |||
| 22 | 25 | /// `GET /git/{owner}/{repo}/raw/{ref}/{*path}`: raw file download. | |
| 23 | 26 | #[tracing::instrument(skip_all, name = "git::raw_file")] | |
| 24 | 27 | pub(super) async fn raw_file( | |
| 25 | - | State(state): State<AppState>, | |
| 28 | + | State(db): State<PgPool>, | |
| 29 | + | State(config): State<Config>, | |
| 26 | 30 | MaybeUserVerified(maybe_user): MaybeUserVerified, | |
| 27 | 31 | Path((owner, repo_name, git_ref, path)): Path<(String, String, String, String)>, | |
| 28 | 32 | ) -> Result<Response> { | |
| 29 | - | let resolved = resolve_repo(&state, &owner, &repo_name, maybe_user.as_ref().map(|u| u.id)).await?; | |
| 33 | + | let resolved = resolve_repo(&db, &config,&owner, &repo_name, maybe_user.as_ref().map(|u| u.id)).await?; | |
| 30 | 34 | ||
| 31 | 35 | let git_ref_c = git_ref.clone(); | |
| 32 | 36 | let path_c = path.clone(); | |
| @@ -99,7 +103,8 @@ pub(super) struct InfoRefsQuery { | |||
| 99 | 103 | /// `GET /git/{owner}/{repo}/info/refs?service=git-upload-pack` | |
| 100 | 104 | #[tracing::instrument(skip_all, name = "git::smart_http_info_refs")] | |
| 101 | 105 | pub(super) async fn smart_http_info_refs( | |
| 102 | - | State(state): State<AppState>, | |
| 106 | + | State(db): State<PgPool>, | |
| 107 | + | State(config): State<Config>, | |
| 103 | 108 | MaybeUserVerified(maybe_user): MaybeUserVerified, | |
| 104 | 109 | Path((owner, repo_name)): Path<(String, String)>, | |
| 105 | 110 | Query(query): Query<InfoRefsQuery>, | |
| @@ -113,16 +118,16 @@ pub(super) async fn smart_http_info_refs( | |||
| 113 | 118 | // Resolve session OR personal-access-token (Basic auth) so `git clone`/push | |
| 114 | 119 | // of a private repo works from the CLI, not just a logged-in browser. | |
| 115 | 120 | let principal = super::resolve_git_http_principal( | |
| 116 | - | &state, &headers, maybe_user.as_ref().map(|u| u.id), | |
| 121 | + | &db, &headers, maybe_user.as_ref().map(|u| u.id), | |
| 117 | 122 | ).await; | |
| 118 | 123 | let repo_name = resolve_repo_name(&repo_name); | |
| 119 | - | let resolved = resolve_repo(&state, &owner, repo_name, principal.as_ref().map(|p| p.user_id)).await?; | |
| 124 | + | let resolved = resolve_repo(&db, &config,&owner, repo_name, principal.as_ref().map(|p| p.user_id)).await?; | |
| 120 | 125 | ||
| 121 | 126 | // The receive-pack (push) advertisement requires write authorization; the | |
| 122 | 127 | // upload-pack (read) advertisement only needs the read access resolve_repo | |
| 123 | 128 | // already enforced. | |
| 124 | 129 | if service == "git-receive-pack" { | |
| 125 | - | authorize_push(&state, &resolved, principal.as_ref()).await?; | |
| 130 | + | authorize_push(&db, &resolved, principal.as_ref()).await?; | |
| 126 | 131 | } | |
| 127 | 132 | ||
| 128 | 133 | // The ref advertisement is NOT gated by `git_smart_http_semaphore`: it's a | |
| @@ -208,7 +213,7 @@ fn require_push_token( | |||
| 208 | 213 | /// The git CLI always authenticates over HTTP with a PAT, so requiring one does | |
| 209 | 214 | /// not regress any real push path. | |
| 210 | 215 | async fn authorize_push( | |
| 211 | - | state: &AppState, | |
| 216 | + | db: &PgPool, | |
| 212 | 217 | resolved: &super::ResolvedRepo, | |
| 213 | 218 | principal: Option<&super::GitHttpPrincipal>, | |
| 214 | 219 | ) -> Result<()> { | |
| @@ -216,7 +221,7 @@ async fn authorize_push( | |||
| 216 | 221 | let is_owner = resolved.db_user.id == principal.user_id; | |
| 217 | 222 | if !is_owner { | |
| 218 | 223 | let can_push = crate::db::repo_collaborators::can_user_push( | |
| 219 | - | &state.db, resolved.db_repo.id, principal.user_id, | |
| 224 | + | db, resolved.db_repo.id, principal.user_id, | |
| 220 | 225 | ).await?; | |
| 221 | 226 | if !can_push { | |
| 222 | 227 | return Err(AppError::Forbidden); | |
| @@ -228,26 +233,27 @@ async fn authorize_push( | |||
| 228 | 233 | /// `POST /git/{owner}/{repo}/git-upload-pack` | |
| 229 | 234 | #[tracing::instrument(skip_all, name = "git::smart_http_upload_pack")] | |
| 230 | 235 | pub(super) async fn smart_http_upload_pack( | |
| 231 | - | State(state): State<AppState>, | |
| 236 | + | State(db): State<PgPool>, | |
| 237 | + | State(config): State<Config>, | |
| 238 | + | State(git): State<Git>, | |
| 232 | 239 | MaybeUserVerified(maybe_user): MaybeUserVerified, | |
| 233 | 240 | Path((owner, repo_name)): Path<(String, String)>, | |
| 234 | 241 | headers: axum::http::HeaderMap, | |
| 235 | 242 | body: axum::body::Bytes, | |
| 236 | 243 | ) -> Result<Response> { | |
| 237 | 244 | let principal = super::resolve_git_http_principal( | |
| 238 | - | &state, &headers, maybe_user.as_ref().map(|u| u.id), | |
| 245 | + | &db, &headers, maybe_user.as_ref().map(|u| u.id), | |
| 239 | 246 | ).await; | |
| 240 | 247 | let repo_name = resolve_repo_name(&repo_name); | |
| 241 | - | resolve_repo(&state, &owner, repo_name, principal.as_ref().map(|p| p.user_id)).await?; | |
| 248 | + | resolve_repo(&db, &config,&owner, repo_name, principal.as_ref().map(|p| p.user_id)).await?; | |
| 242 | 249 | ||
| 243 | - | let root = repos_root(&state)?; | |
| 250 | + | let root = repos_root(&config)?; | |
| 244 | 251 | let repo_path = git::repo_disk_path(&root, &owner, repo_name)?; | |
| 245 | 252 | ||
| 246 | 253 | // One permit for the whole clone. Held until the child exits (see the reaper | |
| 247 | 254 | // task below), so the cap bounds concurrent `upload-pack` processes — | |
| 248 | 255 | // previously a burst of clones fanned out unbounded. | |
| 249 | - | let permit = state | |
| 250 | - | .limiters.git_smart_http_semaphore | |
| 256 | + | let permit = git.smart_http_semaphore | |
| 251 | 257 | .clone() | |
| 252 | 258 | .acquire_owned() | |
| 253 | 259 | .await | |
| @@ -318,18 +324,20 @@ pub(super) async fn smart_http_upload_pack( | |||
| 318 | 324 | /// streamed back out. Mirrors the SSH receive-pack path's authz model. | |
| 319 | 325 | #[tracing::instrument(skip_all, name = "git::smart_http_receive_pack")] | |
| 320 | 326 | pub(super) async fn smart_http_receive_pack( | |
| 321 | - | State(state): State<AppState>, | |
| 327 | + | State(db): State<PgPool>, | |
| 328 | + | State(config): State<Config>, | |
| 329 | + | State(git): State<Git>, | |
| 322 | 330 | MaybeUserVerified(maybe_user): MaybeUserVerified, | |
| 323 | 331 | Path((owner, repo_name)): Path<(String, String)>, | |
| 324 | 332 | headers: axum::http::HeaderMap, | |
| 325 | 333 | body: axum::body::Body, | |
| 326 | 334 | ) -> Result<Response> { | |
| 327 | 335 | let principal = super::resolve_git_http_principal( | |
| 328 | - | &state, &headers, maybe_user.as_ref().map(|u| u.id), | |
| 336 | + | &db, &headers, maybe_user.as_ref().map(|u| u.id), | |
| 329 | 337 | ).await; | |
| 330 | 338 | let repo_name = resolve_repo_name(&repo_name); | |
| 331 | - | let resolved = resolve_repo(&state, &owner, repo_name, principal.as_ref().map(|p| p.user_id)).await?; | |
| 332 | - | authorize_push(&state, &resolved, principal.as_ref()).await?; | |
| 339 | + | let resolved = resolve_repo(&db, &config,&owner, repo_name, principal.as_ref().map(|p| p.user_id)).await?; | |
| 340 | + | authorize_push(&db, &resolved, principal.as_ref()).await?; | |
| 333 | 341 | ||
| 334 | 342 | // Per-account disk quota, same guard the SSH push path enforces. The owner | |
| 335 | 343 | // dir is the parent of `{root}/{owner}/{repo}.git` — a canonicalized, root- | |
| @@ -342,8 +350,7 @@ pub(super) async fn smart_http_receive_pack( | |||
| 342 | 350 | .ok_or_else(|| AppError::Internal(anyhow::anyhow!("git repo path has no owner dir")))?; | |
| 343 | 351 | git::enforce_disk_quota(owner_dir).await?; | |
| 344 | 352 | ||
| 345 | - | let permit = state | |
| 346 | - | .limiters.git_smart_http_semaphore | |
| 353 | + | let permit = git.smart_http_semaphore | |
| 347 | 354 | .clone() | |
| 348 | 355 | .acquire_owned() | |
| 349 | 356 | .await |
| @@ -36,7 +36,7 @@ pub(super) async fn issue_list( | |||
| 36 | 36 | Path((owner, repo_name)): Path<(String, String)>, | |
| 37 | 37 | Query(query): Query<IssueListQuery>, | |
| 38 | 38 | ) -> Result<impl IntoResponse> { | |
| 39 | - | let resolved = super::resolve_repo(&state, &owner, &repo_name, maybe_user.as_ref().map(|u| u.id)).await?; | |
| 39 | + | let resolved = super::resolve_repo(&state.db, &state.config, &owner, &repo_name, maybe_user.as_ref().map(|u| u.id)).await?; | |
| 40 | 40 | ||
| 41 | 41 | let status_filter = match query.status.as_deref() { | |
| 42 | 42 | Some("closed") => Some(IssueStatus::Closed), | |
| @@ -90,7 +90,7 @@ pub(super) async fn issue_detail( | |||
| 90 | 90 | MaybeUserVerified(maybe_user): MaybeUserVerified, | |
| 91 | 91 | Path((owner, repo_name, number)): Path<(String, String, i32)>, | |
| 92 | 92 | ) -> Result<impl IntoResponse> { | |
| 93 | - | let resolved = super::resolve_repo(&state, &owner, &repo_name, maybe_user.as_ref().map(|u| u.id)).await?; | |
| 93 | + | let resolved = super::resolve_repo(&state.db, &state.config, &owner, &repo_name, maybe_user.as_ref().map(|u| u.id)).await?; | |
| 94 | 94 | ||
| 95 | 95 | let issue = db::issues::get_issue_by_number(&state.db, resolved.db_repo.id, number) | |
| 96 | 96 | .await? |
| @@ -29,7 +29,7 @@ pub fn git_issue_routes() -> CsrfRouter<AppState> { | |||
| 29 | 29 | ||
| 30 | 30 | /// Get the default branch name for a repo (for nav bar links). | |
| 31 | 31 | async fn default_ref(state: &AppState, owner: &str, repo_name: &str) -> String { | |
| 32 | - | let root = match repos_root(state) { | |
| 32 | + | let root = match repos_root(&state.config) { | |
| 33 | 33 | Ok(r) => r, | |
| 34 | 34 | Err(_) => return "main".to_string(), | |
| 35 | 35 | }; |
| @@ -138,7 +138,7 @@ pub(super) async fn process_push( | |||
| 138 | 138 | // it inline parks a tokio worker thread for the duration (Run #2 Perf SERIOUS). | |
| 139 | 139 | // The walk creates and drops all git2 types internally and returns only the | |
| 140 | 140 | // owned, Send `Vec` of collected refs. | |
| 141 | - | let root = repos_root(&state)?; | |
| 141 | + | let root = repos_root(&state.config)?; | |
| 142 | 142 | let repo_owner = req.repo_owner.clone(); | |
| 143 | 143 | let repo_name = req.repo_name.clone(); | |
| 144 | 144 | let after = req.after.clone(); |
| @@ -29,7 +29,7 @@ pub(super) async fn repo_settings_form( | |||
| 29 | 29 | AuthUser(user): AuthUser, | |
| 30 | 30 | Path((owner, repo_name)): Path<(String, String)>, | |
| 31 | 31 | ) -> Result<impl IntoResponse> { | |
| 32 | - | let resolved = super::resolve_repo(&state, &owner, &repo_name, Some(user.id)).await?; | |
| 32 | + | let resolved = super::resolve_repo(&state.db, &state.config, &owner, &repo_name, Some(user.id)).await?; | |
| 33 | 33 | ||
| 34 | 34 | if user.id != resolved.db_user.id { | |
| 35 | 35 | return Err(AppError::Forbidden); | |
| @@ -70,7 +70,7 @@ pub(super) async fn repo_settings_save( | |||
| 70 | 70 | Form(form): Form<RepoSettingsForm>, | |
| 71 | 71 | ) -> Result<impl IntoResponse> { | |
| 72 | 72 | user.check_not_suspended()?; | |
| 73 | - | let resolved = super::resolve_repo(&state, &owner, &repo_name, Some(user.id)).await?; | |
| 73 | + | let resolved = super::resolve_repo(&state.db, &state.config, &owner, &repo_name, Some(user.id)).await?; | |
| 74 | 74 | ||
| 75 | 75 | if user.id != resolved.db_user.id { | |
| 76 | 76 | return Err(AppError::Forbidden); | |
| @@ -118,7 +118,7 @@ pub(super) async fn repo_settings_delete( | |||
| 118 | 118 | Path((owner, repo_name)): Path<(String, String)>, | |
| 119 | 119 | ) -> Result<impl IntoResponse> { | |
| 120 | 120 | user.check_not_suspended()?; | |
| 121 | - | let resolved = super::resolve_repo(&state, &owner, &repo_name, Some(user.id)).await?; | |
| 121 | + | let resolved = super::resolve_repo(&state.db, &state.config, &owner, &repo_name, Some(user.id)).await?; | |
| 122 | 122 | ||
| 123 | 123 | if user.id != resolved.db_user.id { | |
| 124 | 124 | return Err(AppError::Forbidden); |