fabrica

hanna/fabrica

3503 bytes
Raw
1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
5//! `fabrica hook post-receive` — the git hook entry point (internal, hidden).
6//!
7//! Invoked by the `post-receive` hook the server installs at repo init. It updates
8//! `pushed_at` and `size_bytes` for the pushed repo. A hook failure **must not**
9//! reject a push git already accepted, so this always exits `0`, logging loudly on
10//! error.
11
12use std::io::Read as _;
13use std::path::{Path, PathBuf};
14use std::process::ExitCode;
15
16use clap::Subcommand;
17
18use crate::context::{block_on, open_store};
19
20/// Actions under the hidden `fabrica hook` group.
21#[derive(Debug, Subcommand)]
22pub(crate) enum HookCommand {
23 /// Run after a push is accepted.
24 PostReceive,
25}
26
27/// Dispatch a hook subcommand. Always exits successfully.
28pub(crate) fn run(command: &HookCommand) -> ExitCode {
29 match command {
30 HookCommand::PostReceive => {
31 if let Err(err) = post_receive() {
32 // Loud, but non-fatal: the push has already been accepted.
33 eprintln!("fabrica hook post-receive: {err:#}");
34 }
35 ExitCode::SUCCESS
36 }
37 }
38}
39
40/// The fallible body of `post-receive`.
41fn post_receive() -> anyhow::Result<()> {
42 // Drain stdin (`<old> <new> <ref>` lines) so git does not see a broken pipe,
43 // even though we do not currently act per-ref.
44 let mut input = String::new();
45 std::io::stdin().read_to_string(&mut input)?;
46
47 // git runs post-receive with cwd = the repo dir and often GIT_DIR=".";
48 // canonicalize so we get the absolute `{id}.git` directory either way.
49 let git_dir = std::env::var_os("GIT_DIR").map_or_else(|| PathBuf::from("."), PathBuf::from);
50 let git_dir = std::fs::canonicalize(&git_dir).unwrap_or(git_dir);
51
52 // The bare repo dir is `{id}.git`; the stem is the repo's ULID.
53 let Some(repo_id) = git_dir
54 .file_name()
55 .and_then(|n| n.to_str())
56 .and_then(|n| n.strip_suffix(".git"))
57 .filter(|id| id.len() == 26)
58 else {
59 // Not a fabrica-managed repository; nothing to do.
60 return Ok(());
61 };
62 let repo_id = repo_id.to_string();
63
64 let config_path = std::env::var_os("FABRICA_CONFIG").map(PathBuf::from);
65 let size = dir_size(&git_dir);
66
67 block_on(async move {
68 let (config, store) = open_store(config_path.as_deref()).await?;
69 store.record_push(&repo_id, size).await?;
70 // Propagate the push to any "sync on push" mirrors (best-effort).
71 mirror::trigger_push_mirrors(&store, &config, &repo_id).await;
72 anyhow::Ok(())
73 })
74}
75
76/// Sum the sizes of every regular file under `dir` (an approximate on-disk size).
77fn dir_size(dir: &Path) -> i64 {
78 fn walk(dir: &Path, total: &mut u64) {
79 let Ok(entries) = std::fs::read_dir(dir) else {
80 return;
81 };
82 for entry in entries.flatten() {
83 let path = entry.path();
84 match entry.file_type() {
85 Ok(ft) if ft.is_dir() => walk(&path, total),
86 Ok(ft) if ft.is_file() => {
87 if let Ok(meta) = entry.metadata() {
88 *total += meta.len();
89 }
90 }
91 _ => {}
92 }
93 }
94 }
95 let mut total = 0u64;
96 walk(dir, &mut total);
97 i64::try_from(total).unwrap_or(i64::MAX)
98}