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/// Debug logging of an omitted entry.
15use super::diagnostic_log::debug;
16/// The name of the absent hooks directory, shared with the shadow commands.
17use super::shadow_git::NO_HOOKS_DIRECTORY;
18/// Plan spelling shared with the reader.
19use super::hook_dispatch_plan::{
20    HookDispatchPlan, PLAN_FILENAME, PREPARATION_HOOK_EVENTS, encode_hook_dispatch_plan,
21};
22/// Private directory creation and exclusive private file writes.
23use super::private_storage::{create_private_directory, write_private_file};
24/// `Path`/`PathBuf` are borrowed/owned filesystem paths.
25use std::path::{Path, PathBuf};
26
27/// What:
28///  Why writing a hooks directory failed, naming the path.
29/// Why:
30///   The caller stops the transaction; a partial hooks directory is never reused.
31///
32/// In TS you'd write (pseudocode):
33/// ```ts
34/// throw new Error(`cannot write ${path}`);
35/// ```
36#[derive(Clone, Debug, Eq, PartialEq)]
37pub struct HookEntryError(pub String);
38
39/// What:
40///  One sentence naming the hook entry that failed.
41/// Why:
42///   The caller prints it as the diagnostic.
43///
44/// In TS you'd write (pseudocode):
45/// ```ts
46/// error.message
47/// ```
48impl std::fmt::Display for HookEntryError {
49    /// Writes the stored sentence.
50    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
51        return write!(formatter, "{}", self.0);
52    }
53}
54
55/// What:
56///  The file name of one preparation event's entry.
57/// Why:
58///   Unix names the entry after the event; Windows adds `.exe`, which Git appends when it looks up a hook.
59///
60/// In TS you'd write (pseudocode):
61/// ```ts
62/// process.platform === 'win32' ? `${event}.exe` : event;
63/// ```
64pub fn hook_entry_name(event: &str) -> String {
65    if cfg!(windows) {
66        return format!("{event}.exe");
67    }
68    return String::from(event);
69}
70
71/// What:
72///  The hooks directory that is deliberately absent, beside the transaction's dispatcher directory.
73/// Why:
74///   The dispatcher's inner `git hook run` and the post-commit run point `core.hooksPath` here, so Git finds no
75///  traditional hook (unsupported) and never starts the dispatcher entries again.
76///  The directory is never created, because Git treats a missing hooks directory as holding no hook.
77///
78/// In TS you'd write (pseudocode):
79/// ```ts
80/// join(transactionDirectory, 'no-hooks')
81/// ```
82pub fn transaction_no_hooks_directory(transaction_directory: &Path) -> PathBuf {
83    return transaction_directory.join(NO_HOOKS_DIRECTORY);
84}
85
86/// What:
87///  Write one entry that starts `executable` under its event's name.
88/// Why:
89///   Unix symbolic links keep the start name as `argv[0]`;
90///  Windows entries are hard links, or copies where hard links are refused.
91///
92/// In TS you'd write (pseudocode):
93/// ```ts
94/// await writeFile(join(hooksDirectory, event), hookEntryProgram({ nodePath, dispatchUrl, event }));
95/// ```
96fn write_entry(executable: &Path, entry: &Path) -> std::io::Result<()> {
97    #[cfg(unix)]
98    {
99        std::os::unix::fs::symlink(executable, entry)?;
100    }
101    // UNTESTED ON WINDOWS: no gate compiles or runs this branch (the crate does not build for Windows; README, Requirements).
102    #[cfg(windows)]
103    {
104        if std::fs::hard_link(executable, entry).is_err() {
105            std::fs::copy(executable, entry)?;
106        }
107    }
108    return Ok(());
109}
110
111/// What:
112///  Create a new hooks directory holding the plan and one entry per preparation event.
113/// Why:
114///   The incumbent's `writeHookShim`, with native entries instead of a Node program:
115///  `post-commit` has no entry, so it never runs during preparation.
116///  A configuration that defines no hook by name gets no entry at all: Git then finds nothing to start, which is what
117///  an entry would have done after re-entering this executable, taking the hook lock and running `git hook run`.
118///  The directory must not exist, so nothing half-written is reused.
119///
120/// In TS you'd write (pseudocode):
121/// ```ts
122/// await writeHookShim({ hooksDirectory, plan, nodePath });
123/// ```
124pub fn write_hook_dispatch(
125    hooks_directory: &Path,
126    plan: &HookDispatchPlan,
127    executable: &Path,
128) -> Result<(), HookEntryError> {
129    create_private_directory(hooks_directory).map_err(|error| {
130        return HookEntryError(format!(
131            "cannot create the hook directory {}: {error}",
132            hooks_directory.display()
133        ))
134    })?;
135    let plan_path: PathBuf = hooks_directory.join(PLAN_FILENAME);
136    let plan_bytes: String = format!("{}\n", encode_hook_dispatch_plan(plan));
137    write_private_file(plan_path.as_path(), plan_bytes.as_bytes()).map_err(|error| {
138        return HookEntryError(format!(
139            "cannot write the hook plan {}: {error}",
140            plan_path.display()
141        ))
142    })?;
143    if !plan.named_hooks_possible {
144        debug("writeHookDispatch", "the configuration defines no hook by name; no entry is written");
145        return Ok(());
146    }
147    for event in PREPARATION_HOOK_EVENTS {
148        let entry: PathBuf = hooks_directory.join(hook_entry_name(event));
149        write_entry(executable, entry.as_path()).map_err(|error| {
150            return HookEntryError(format!(
151                "cannot write the hook entry {}: {error}",
152                entry.display()
153            ))
154        })?;
155    }
156    return Ok(());
157}
158
159#[cfg(test)]
160#[path = "hook_dispatch_entries_tests.rs"]
161mod tests;