forge
ExploreLog in

Tahinli/forge

public
0 watching
★ 0
4.2 KiB
1//! Managed gitconfig and hook shims, written into the data dir at boot.
2
3use std::fs::{self, File};
4use std::io::Write;
5use std::os::unix::fs::PermissionsExt;
6use std::path::Path;
7
8use crate::error::{Error, Result};
9
10/// Quotes a value for a git config file. Control characters are rejected.
11fn quote_config_value(v: &str) -> Result<String> {
12 if v.chars().any(char::is_control) {
13 return Err(Error::InvalidInput(format!(
14 "config value contains control characters: {v:?}"
15 )));
16 }
17 Ok(format!(
18 "\"{}\"",
19 v.replace('\\', "\\\\").replace('"', "\\\"")
20 ))
21}
22
23/// Writes `name` in `dir` atomically: temp file, fsync, rename, then fsync the directory.
24fn write_atomic(dir: &Path, name: &str, contents: &[u8], mode: u32) -> Result<()> {
25 let tmp = dir.join(format!(".{name}.tmp"));
26 {
27 let mut f = fs::OpenOptions::new()
28 .write(true)
29 .create(true)
30 .truncate(true)
31 .open(&tmp)?;
32 f.write_all(contents)?;
33 f.set_permissions(fs::Permissions::from_mode(mode))?;
34 f.sync_all()?;
35 }
36 fs::rename(&tmp, dir.join(name))?;
37 File::open(dir)?.sync_all()?;
38 Ok(())
39}
40
41/// Writes `data_dir/gitconfig`: core.hooksPath, fsck, fsync, uploadpack and receive settings.
42pub fn write_managed_gitconfig(data_dir: &Path) -> Result<()> {
43 if !data_dir.is_absolute() {
44 return Err(Error::InvalidInput(format!(
45 "data dir must be absolute: {}",
46 data_dir.display()
47 )));
48 }
49 let hooks = data_dir.join("hooks");
50 let hooks = hooks
51 .to_str()
52 .ok_or_else(|| Error::InvalidInput("data dir is not valid UTF-8".into()))?;
53 let hooks = quote_config_value(hooks)?;
54
55 // `committed` hardens loose objects and packs; `reference` hardens ref updates.
56 let body = format!(
57 "[core]\n\
58 \thooksPath = {hooks}\n\
59 \tfsync = committed,reference\n\
60 \tfsyncMethod = fsync\n\
61 \tcommitGraph = true\n\
62 [receive]\n\
63 \tfsckObjects = true\n\
64 \tadvertisePushOptions = true\n\
65 \tdenyNonFastForwards = false\n\
66 [transfer]\n\
67 \tfsckObjects = true\n\
68 \thideRefs = refs/forge-internal/\n\
69 [fetch]\n\
70 \tfsckObjects = true\n\
71 \twriteCommitGraph = true\n\
72 [uploadpack]\n\
73 \tallowFilter = true\n\
74 \tallowRefInWant = true\n\
75 \thideRefs = refs/forge-internal/\n\
76 [protocol \"file\"]\n\
77 \tallow = never\n\
78 [protocol \"ext\"]\n\
79 \tallow = never\n"
80 );
81 fs::create_dir_all(data_dir)?;
82 write_atomic(data_dir, "gitconfig", body.as_bytes(), 0o644)
83}
84
85/// Writes `data_dir/hooks/{pre-receive,post-receive}` as `exec <forge_bin> hook <name>`, mode 0755.
86/// `forge_bin` must be absolute and contain no quotes or control characters.
87pub fn write_hook_shims(data_dir: &Path, forge_bin: &Path) -> Result<()> {
88 let bin = forge_bin
89 .to_str()
90 .ok_or_else(|| Error::InvalidInput("forge binary path is not valid UTF-8".into()))?;
91 if !forge_bin.is_absolute() {
92 return Err(Error::InvalidInput(format!(
93 "forge binary path must be absolute: {bin}"
94 )));
95 }
96 if bin.chars().any(|c| c == '\'' || c.is_control()) {
97 return Err(Error::InvalidInput(format!(
98 "forge binary path contains a quote or control character: {bin:?}"
99 )));
100 }
101 let hooks = data_dir.join("hooks");
102 fs::create_dir_all(&hooks)?;
103 for name in ["pre-receive", "post-receive"] {
104 let body = format!("#!/bin/sh\nexec '{bin}' hook {name}\n");
105 write_atomic(&hooks, name, body.as_bytes(), 0o755)?;
106 }
107 Ok(())
108}
109
110#[cfg(test)]
111mod tests {
112 use super::*;
113
114 #[test]
115 fn managed_gitconfig_enables_commit_graph() {
116 let tmp = tempfile::tempdir().expect("tempdir");
117 write_managed_gitconfig(tmp.path()).expect("write gitconfig");
118 let body = fs::read_to_string(tmp.path().join("gitconfig")).expect("read gitconfig");
119 assert!(body.contains("\tcommitGraph = true\n"));
120 assert!(body.contains("\twriteCommitGraph = true\n"));
121 assert!(
122 !body.contains("gc.auto"),
123 "gc.auto must stay at its default"
124 );
125 }
126}