Skip to main content

git_policy_cli/
hook_dispatch_entries.rs

1//! What:
2//!  The hooks directory of one commit transaction:
3//!  its `plan.json` and one entry per preparation event.
4//! Why:
5//!  Git starts a hook through `core.hooksPath` by the entry's path, so the entry's name is the event
6//!  and the executable it starts is the native wrapper.
7//!  On Windows Git looks for `<event>.exe`, so entries are hard links or copies with that name.
8//!
9//! In TS you'd write (pseudocode):
10//! ```ts
11//! await writeHookShim({ hooksDirectory, plan, nodePath: process.execPath });
12//! ```
13
14/// Plan spelling shared with the reader.
15use super::hook_dispatch_plan::{
16    HookDispatchPlan, PLAN_FILENAME, PREPARATION_HOOK_EVENTS, encode_hook_dispatch_plan,
17};
18/// Private directory creation and exclusive private file writes.
19use super::private_storage::{create_private_directory, write_private_file};
20/// `Path`/`PathBuf` are borrowed/owned filesystem paths.
21use std::path::{Path, PathBuf};
22
23/// What:
24///  Why writing a hooks directory failed, naming the path.
25/// Why:
26///   The caller stops the transaction; a partial hooks directory is never reused.
27///
28/// In TS you'd write (pseudocode):
29/// ```ts
30/// throw new Error(`cannot write ${path}`);
31/// ```
32#[derive(Clone, Debug, Eq, PartialEq)]
33pub struct HookEntryError(pub String);
34
35/// What:
36///  One sentence naming the hook entry that failed.
37/// Why:
38///   The caller prints it as the diagnostic.
39///
40/// In TS you'd write (pseudocode):
41/// ```ts
42/// error.message
43/// ```
44impl std::fmt::Display for HookEntryError {
45    /// Writes the stored sentence.
46    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
47        return write!(formatter, "{}", self.0);
48    }
49}
50
51/// What:
52///  The file name of one preparation event's entry.
53/// Why:
54///   Unix names the entry after the event; Windows adds `.exe`, which Git appends when it looks up a hook.
55///
56/// In TS you'd write (pseudocode):
57/// ```ts
58/// process.platform === 'win32' ? `${event}.exe` : event;
59/// ```
60pub fn hook_entry_name(event: &str) -> String {
61    if cfg!(windows) {
62        return format!("{event}.exe");
63    }
64    return String::from(event);
65}
66
67/// What:
68///  Write one entry that starts `executable` under its event's name.
69/// Why:
70///   Unix symbolic links keep the start name as `argv[0]`;
71///  Windows entries are hard links, or copies where hard links are refused.
72///
73/// In TS you'd write (pseudocode):
74/// ```ts
75/// await writeFile(join(hooksDirectory, event), hookEntryProgram({ nodePath, dispatchUrl, event }));
76/// ```
77fn write_entry(executable: &Path, entry: &Path) -> std::io::Result<()> {
78    #[cfg(unix)]
79    {
80        std::os::unix::fs::symlink(executable, entry)?;
81    }
82    // UNTESTED ON WINDOWS: no gate compiles or runs this branch (the crate does not build for Windows; README, Requirements).
83    #[cfg(windows)]
84    {
85        if std::fs::hard_link(executable, entry).is_err() {
86            std::fs::copy(executable, entry)?;
87        }
88    }
89    return Ok(());
90}
91
92/// What:
93///  Create a new hooks directory holding the plan and one entry per preparation event.
94/// Why:
95///   The incumbent's `writeHookShim`, with native entries instead of a Node program:
96///  `post-commit` has no entry, so it never runs during preparation.
97///  The directory must not exist, so nothing half-written is reused.
98///
99/// In TS you'd write (pseudocode):
100/// ```ts
101/// await writeHookShim({ hooksDirectory, plan, nodePath });
102/// ```
103pub fn write_hook_dispatch(
104    hooks_directory: &Path,
105    plan: &HookDispatchPlan,
106    executable: &Path,
107) -> Result<(), HookEntryError> {
108    create_private_directory(hooks_directory).map_err(|error| {
109        return HookEntryError(format!(
110            "cannot create the hook directory {}: {error}",
111            hooks_directory.display()
112        ))
113    })?;
114    let plan_path: PathBuf = hooks_directory.join(PLAN_FILENAME);
115    let plan_bytes: String = format!("{}\n", encode_hook_dispatch_plan(plan));
116    write_private_file(plan_path.as_path(), plan_bytes.as_bytes()).map_err(|error| {
117        return HookEntryError(format!(
118            "cannot write the hook plan {}: {error}",
119            plan_path.display()
120        ))
121    })?;
122    for event in PREPARATION_HOOK_EVENTS {
123        let entry: PathBuf = hooks_directory.join(hook_entry_name(event));
124        write_entry(executable, entry.as_path()).map_err(|error| {
125            return HookEntryError(format!(
126                "cannot write the hook entry {}: {error}",
127                entry.display()
128            ))
129        })?;
130    }
131    return Ok(());
132}
133
134#[cfg(test)]
135#[path = "hook_dispatch_entries_tests.rs"]
136mod tests;