Refactor: Formalize ports and update related files
This commit is contained in:
@@ -14,6 +14,10 @@ pub enum HookResult {
|
||||
/// Run all hooks matching `phase` and collect into a `Report`.
|
||||
///
|
||||
/// State is loaded once and saved once per call — not once per hook.
|
||||
pub fn run_phase(hooks: &[HookSpec], phase: &HookPhase, runner: &HookRunner) -> Report {
|
||||
pub fn run_phase(
|
||||
hooks: &[HookSpec],
|
||||
phase: &HookPhase,
|
||||
runner: &HookRunner,
|
||||
) -> anyhow::Result<Report> {
|
||||
runner.run_phase(hooks, phase)
|
||||
}
|
||||
|
||||
@@ -62,7 +62,7 @@ fn main() -> Result<()> {
|
||||
HookRunner::new(state_dir)
|
||||
};
|
||||
|
||||
let report = run_phase(&file.hooks, &phase, &runner);
|
||||
let report = run_phase(&file.hooks, &phase, &runner)?;
|
||||
report.print();
|
||||
|
||||
if report.has_failures() {
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
use crate::HookResult;
|
||||
use crate::state::HookState;
|
||||
use anyhow::{Context, Result};
|
||||
use notcore::{HookPhase, HookSpec, Report, StepStatus};
|
||||
use std::path::PathBuf;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::Command;
|
||||
|
||||
fn resolve_interpreter(spec: &HookSpec) -> Result<String, String> {
|
||||
@@ -51,8 +52,9 @@ impl HookRunner {
|
||||
/// State is loaded once before iterating and saved once at the end (only if
|
||||
/// at least one Setup hook completed successfully), avoiding N file reads
|
||||
/// and writes per phase.
|
||||
pub fn run_phase(&self, hooks: &[HookSpec], phase: &HookPhase) -> Report {
|
||||
let mut state = HookState::load(&self.state_dir).unwrap_or_default();
|
||||
pub fn run_phase(&self, hooks: &[HookSpec], phase: &HookPhase) -> Result<Report> {
|
||||
let mut state = HookState::load(&self.state_dir)
|
||||
.with_context(|| format!("loading hook state from {}", self.state_dir.display()))?;
|
||||
let mut state_dirty = false;
|
||||
let mut report = Report::default();
|
||||
|
||||
@@ -67,10 +69,12 @@ impl HookRunner {
|
||||
}
|
||||
|
||||
if state_dirty {
|
||||
let _ = state.save(&self.state_dir);
|
||||
state
|
||||
.save(&self.state_dir)
|
||||
.with_context(|| format!("saving hook state into {}", self.state_dir.display()))?;
|
||||
}
|
||||
|
||||
report
|
||||
Ok(report)
|
||||
}
|
||||
|
||||
/// Execute a single hook, using the caller-owned `state` and `dirty` flag
|
||||
@@ -89,11 +93,12 @@ impl HookRunner {
|
||||
Ok(i) => i,
|
||||
Err(msg) => return HookResult::Failed(msg),
|
||||
};
|
||||
let script = self.resolve_script_path(&spec.script);
|
||||
// SAFETY: `interp` is derived from the file extension or an explicit `interpreter` field
|
||||
// in HookSpec (both come from config, not untrusted shell input). `spec.script` is a
|
||||
// filesystem path from config. Both are passed as discrete args with no shell involved,
|
||||
// so argument injection is not possible.
|
||||
let result = Command::new(&interp).arg(&spec.script).status();
|
||||
let result = Command::new(&interp).arg(&script).status();
|
||||
|
||||
match result {
|
||||
Ok(status) if status.success() => {
|
||||
@@ -112,13 +117,25 @@ impl HookRunner {
|
||||
///
|
||||
/// Prefer [`HookRunner::run_phase`] when running multiple hooks to avoid
|
||||
/// repeated state I/O.
|
||||
pub fn run_hook(&self, spec: &HookSpec) -> HookResult {
|
||||
let mut state = HookState::load(&self.state_dir).unwrap_or_default();
|
||||
pub fn run_hook(&self, spec: &HookSpec) -> Result<HookResult> {
|
||||
let mut state = HookState::load(&self.state_dir)
|
||||
.with_context(|| format!("loading hook state from {}", self.state_dir.display()))?;
|
||||
let mut state_dirty = false;
|
||||
let result = self.run_hook_with_state(spec, &mut state, &mut state_dirty);
|
||||
if state_dirty {
|
||||
let _ = state.save(&self.state_dir);
|
||||
state
|
||||
.save(&self.state_dir)
|
||||
.with_context(|| format!("saving hook state into {}", self.state_dir.display()))?;
|
||||
}
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
fn resolve_script_path(&self, script: &str) -> PathBuf {
|
||||
let script = Path::new(script);
|
||||
if script.is_absolute() {
|
||||
script.to_path_buf()
|
||||
} else {
|
||||
self.state_dir.join(script)
|
||||
}
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,9 @@ impl HookState {
|
||||
|
||||
pub fn save(&self, dir: &Path) -> Result<()> {
|
||||
let path = dir.join(STATE_FILE);
|
||||
std::fs::write(path, toml::to_string(self)?)?;
|
||||
let tmp = dir.join(format!("{STATE_FILE}.tmp"));
|
||||
std::fs::write(&tmp, toml::to_string(self)?)?;
|
||||
std::fs::rename(&tmp, &path)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ fn test_hook_success() {
|
||||
let dir = TempDir::new().unwrap();
|
||||
let spec = make_hook_script(&dir, "ok-hook", "echo hello");
|
||||
let runner = HookRunner::new(dir.path().to_path_buf());
|
||||
let result = runner.run_hook(&spec);
|
||||
let result = runner.run_hook(&spec).unwrap();
|
||||
assert!(matches!(result, HookResult::Ok));
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ fn test_hook_failure() {
|
||||
let dir = TempDir::new().unwrap();
|
||||
let spec = make_hook_script(&dir, "fail-hook", "exit 1");
|
||||
let runner = HookRunner::new(dir.path().to_path_buf());
|
||||
let result = runner.run_hook(&spec);
|
||||
let result = runner.run_hook(&spec).unwrap();
|
||||
assert!(matches!(result, HookResult::Failed(_)));
|
||||
}
|
||||
|
||||
@@ -39,11 +39,11 @@ fn test_setup_hook_skipped_on_rerun() {
|
||||
spec.phase = notcore::HookPhase::Setup;
|
||||
|
||||
let runner = HookRunner::new(dir.path().to_path_buf());
|
||||
let r1 = runner.run_hook(&spec);
|
||||
let r1 = runner.run_hook(&spec).unwrap();
|
||||
assert!(matches!(r1, HookResult::Ok));
|
||||
|
||||
// Second run — should be skipped
|
||||
let r2 = runner.run_hook(&spec);
|
||||
let r2 = runner.run_hook(&spec).unwrap();
|
||||
assert!(matches!(r2, HookResult::Skipped));
|
||||
}
|
||||
|
||||
@@ -54,9 +54,29 @@ fn test_setup_hook_force_reruns() {
|
||||
spec.phase = notcore::HookPhase::Setup;
|
||||
|
||||
let runner = HookRunner::new(dir.path().to_path_buf());
|
||||
runner.run_hook(&spec);
|
||||
runner.run_hook(&spec).unwrap();
|
||||
|
||||
let runner2 = HookRunner::with_force(dir.path().to_path_buf());
|
||||
let r2 = runner2.run_hook(&spec);
|
||||
let r2 = runner2.run_hook(&spec).unwrap();
|
||||
assert!(matches!(r2, HookResult::Ok));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_relative_script_path_resolves_from_state_dir() {
|
||||
let dir = TempDir::new().unwrap();
|
||||
let scripts_dir = dir.path().join("scripts");
|
||||
fs::create_dir_all(&scripts_dir).unwrap();
|
||||
let script = scripts_dir.join("relative.sh");
|
||||
fs::write(&script, "echo relative-ok").unwrap();
|
||||
|
||||
let spec = HookSpec {
|
||||
name: "relative".to_string(),
|
||||
script: "scripts/relative.sh".to_string(),
|
||||
phase: HookPhase::Dot,
|
||||
interpreter: Some("sh".to_string()),
|
||||
};
|
||||
|
||||
let runner = HookRunner::new(dir.path().to_path_buf());
|
||||
let result = runner.run_hook(&spec).unwrap();
|
||||
assert!(matches!(result, HookResult::Ok));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user