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