// This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. //! `fabrica hook post-receive` — the git hook entry point (internal, hidden). //! //! Invoked by the `post-receive` hook the server installs at repo init. It updates //! `pushed_at` and `size_bytes` for the pushed repo. A hook failure **must not** //! reject a push git already accepted, so this always exits `0`, logging loudly on //! error. use std::io::Read as _; use std::path::{Path, PathBuf}; use std::process::ExitCode; use clap::Subcommand; use crate::context::{block_on, open_store}; /// Actions under the hidden `fabrica hook` group. #[derive(Debug, Subcommand)] pub(crate) enum HookCommand { /// Run after a push is accepted. PostReceive, } /// Dispatch a hook subcommand. Always exits successfully. pub(crate) fn run(command: &HookCommand) -> ExitCode { match command { HookCommand::PostReceive => { if let Err(err) = post_receive() { // Loud, but non-fatal: the push has already been accepted. eprintln!("fabrica hook post-receive: {err:#}"); } ExitCode::SUCCESS } } } /// The fallible body of `post-receive`. fn post_receive() -> anyhow::Result<()> { // Drain stdin (` ` lines) so git does not see a broken pipe, // even though we do not currently act per-ref. let mut input = String::new(); std::io::stdin().read_to_string(&mut input)?; // git runs post-receive with cwd = the repo dir and often GIT_DIR="."; // canonicalize so we get the absolute `{id}.git` directory either way. let git_dir = std::env::var_os("GIT_DIR").map_or_else(|| PathBuf::from("."), PathBuf::from); let git_dir = std::fs::canonicalize(&git_dir).unwrap_or(git_dir); // The bare repo dir is `{id}.git`; the stem is the repo's ULID. let Some(repo_id) = git_dir .file_name() .and_then(|n| n.to_str()) .and_then(|n| n.strip_suffix(".git")) .filter(|id| id.len() == 26) else { // Not a fabrica-managed repository; nothing to do. return Ok(()); }; let repo_id = repo_id.to_string(); let config_path = std::env::var_os("FABRICA_CONFIG").map(PathBuf::from); let size = dir_size(&git_dir); block_on(async move { let (config, store) = open_store(config_path.as_deref()).await?; store.record_push(&repo_id, size).await?; // Propagate the push to any "sync on push" mirrors (best-effort). mirror::trigger_push_mirrors(&store, &config, &repo_id).await; anyhow::Ok(()) }) } /// Sum the sizes of every regular file under `dir` (an approximate on-disk size). fn dir_size(dir: &Path) -> i64 { fn walk(dir: &Path, total: &mut u64) { let Ok(entries) = std::fs::read_dir(dir) else { return; }; for entry in entries.flatten() { let path = entry.path(); match entry.file_type() { Ok(ft) if ft.is_dir() => walk(&path, total), Ok(ft) if ft.is_file() => { if let Ok(meta) = entry.metadata() { *total += meta.len(); } } _ => {} } } } let mut total = 0u64; walk(dir, &mut total); i64::try_from(total).unwrap_or(i64::MAX) }