Skip to main content

fren_date/execute/
mod.rs

1//! Execute a sorted plan vector. Reads the bottom-up invariant from
2//! [`crate::plan::sort_bottom_up`]. Performs each rename via the
3//! atomic primitives in [`atomic`] so that pre-existing targets are
4//! never silently overwritten.
5
6mod atomic;
7mod case_only;
8
9use crate::log::{LogRecord, LogSink, NullLogSink};
10use crate::plan_types::ItemKind;
11use crate::{ExecutionReport, FrenError, RenamePlan};
12use chrono::Utc;
13use uuid::Uuid;
14
15/// Callback invoked after each successful rename during execution.
16///
17/// Implement this in the caller (e.g. the CLI) to stream rename output as
18/// each operation completes, rather than buffering and printing after the
19/// full batch. The library calls `on_rename` with the completed plan; the
20/// implementation decides how (or whether) to display it.
21///
22/// A no-op implementation is provided via [`NullProgressSink`].
23pub trait ProgressSink {
24    /// Called immediately after a rename succeeds.
25    fn on_rename(&mut self, plan: &RenamePlan);
26}
27
28/// No-op [`ProgressSink`]. Used by [`execute`] and [`execute_with_log`].
29pub struct NullProgressSink;
30
31impl ProgressSink for NullProgressSink {
32    fn on_rename(&mut self, _plan: &RenamePlan) {}
33}
34
35/// Apply a sorted plan vector with no transaction logging.
36///
37/// Equivalent to [`execute_with_log`] using a [`NullLogSink`]. Convenience
38/// for tests and quick scripts.
39pub fn execute(plans: &[RenamePlan]) -> Result<ExecutionReport, FrenError> {
40    let mut sink = NullLogSink;
41    execute_with_log(plans, &mut sink)
42}
43
44/// Apply a sorted plan vector, recording every rename to `log_sink`.
45pub fn execute_with_log(
46    plans: &[RenamePlan],
47    log_sink: &mut dyn LogSink,
48) -> Result<ExecutionReport, FrenError> {
49    execute_with_progress(plans, log_sink, &mut NullProgressSink)
50}
51
52/// Apply a sorted plan vector, recording to `log_sink` and calling
53/// `progress` after each successful rename.
54///
55/// This is the core executor. [`execute`] and [`execute_with_log`] are
56/// convenience wrappers that supply a [`NullProgressSink`].
57pub fn execute_with_progress(
58    plans: &[RenamePlan],
59    log_sink: &mut dyn LogSink,
60    progress: &mut dyn ProgressSink,
61) -> Result<ExecutionReport, FrenError> {
62    let batch_id = plans.first().map(|p| p.batch_id).unwrap_or_else(Uuid::nil);
63
64    let mut applied = 0usize;
65    let mut errors: Vec<FrenError> = Vec::new();
66
67    for plan in plans {
68        let from = &plan.original_path;
69        let to_path = plan.parent.join(&plan.new_name);
70
71        let outcome = match atomic::rename(from, &to_path) {
72            Ok(()) => Ok(()),
73            Err(FrenError::TargetExists(_)) if case_only::is_case_only_rename(from, &to_path) => {
74                case_only::rename_via_temp(from, &to_path)
75            }
76            Err(e) => Err(e),
77        };
78
79        match outcome {
80            Ok(()) => {
81                applied += 1;
82                progress.on_rename(plan);
83                let kind = match plan.kind {
84                    ItemKind::File => "file",
85                    ItemKind::Dir => "dir",
86                    ItemKind::Symlink => "symlink",
87                };
88                let record = LogRecord::Rename {
89                    v: 1,
90                    ts: Utc::now().to_rfc3339(),
91                    from: from.clone(),
92                    to: to_path,
93                    kind: kind.to_string(),
94                };
95                // Log errors are noted but don't abort the batch.
96                if let Err(e) = log_sink.append(&record) {
97                    errors.push(e);
98                }
99            }
100            Err(e) => {
101                errors.push(e);
102                break; // abort policy: stop on first failure
103            }
104        }
105    }
106
107    let log_path = log_sink.path().map(|p| p.to_path_buf());
108
109    Ok(ExecutionReport {
110        applied,
111        skipped: 0,
112        errors,
113        batch_id,
114        log_path,
115    })
116}