Skip to main content

verbs/
context.rs

1// SPDX-License-Identifier: Apache-2.0
2//! Execution context shared by future facade operations.
3
4use std::{path::PathBuf, sync::Arc};
5
6use objects::{HeddleError, NoopProgress, NoopWarnings, ProgressSink, WarningSink};
7use repo::{FsMonitorMode, Repository, WorktreeStatusOptions};
8
9/// Semantic detail level for facade operations.
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
11pub enum Verbosity {
12    Quiet,
13    #[default]
14    Normal,
15    Verbose,
16}
17
18/// Semantic execution state for embeddable Heddle operations.
19///
20/// Carries only resolved values: the caller (usually the CLI) resolves user
21/// config before construction, so this crate never reads config files.
22pub struct ExecutionContext {
23    repo: Option<Repository>,
24    start_path: Option<PathBuf>,
25    principal_fallback: Option<(String, String)>,
26    fsmonitor_mode: FsMonitorMode,
27    verbosity: Verbosity,
28    progress: Arc<dyn ProgressSink>,
29    warnings: Arc<dyn WarningSink>,
30    op_id: Option<String>,
31    // TODO(F3): faults + semantic_cache once de-singletoned.
32}
33
34impl ExecutionContext {
35    pub fn builder() -> ExecutionContextBuilder {
36        ExecutionContextBuilder::default()
37    }
38
39    pub fn require_repo(&self) -> Result<&Repository, HeddleError> {
40        self.repo
41            .as_ref()
42            .ok_or_else(|| HeddleError::RepositoryNotFound(PathBuf::from(".")))
43    }
44
45    pub fn repo(&self) -> Option<&Repository> {
46        self.repo.as_ref()
47    }
48
49    pub fn start_path(&self) -> Option<&std::path::Path> {
50        self.start_path.as_deref()
51    }
52
53    /// User-config principal fallback as an optional `(name, email)` pair.
54    pub fn principal_fallback(&self) -> Option<(&str, &str)> {
55        self.principal_fallback
56            .as_ref()
57            .map(|(name, email)| (name.as_str(), email.as_str()))
58    }
59
60    /// Resolved fsmonitor mode for worktree-status hot paths.
61    pub fn fsmonitor_mode(&self) -> FsMonitorMode {
62        self.fsmonitor_mode
63    }
64
65    pub fn worktree_status_options(&self) -> WorktreeStatusOptions {
66        WorktreeStatusOptions {
67            fsmonitor: repo::FsMonitorSettings {
68                mode: self.fsmonitor_mode,
69            },
70        }
71    }
72
73    pub fn progress(&self) -> &dyn ProgressSink {
74        &*self.progress
75    }
76
77    pub fn warnings(&self) -> &dyn WarningSink {
78        &*self.warnings
79    }
80
81    pub fn verbosity(&self) -> Verbosity {
82        self.verbosity
83    }
84
85    pub fn op_id(&self) -> Option<&str> {
86        self.op_id.as_deref()
87    }
88}
89
90/// Builder for [`ExecutionContext`].
91pub struct ExecutionContextBuilder {
92    repo: Option<Repository>,
93    start_path: Option<PathBuf>,
94    principal_fallback: Option<(String, String)>,
95    fsmonitor_mode: FsMonitorMode,
96    verbosity: Verbosity,
97    progress: Arc<dyn ProgressSink>,
98    warnings: Arc<dyn WarningSink>,
99    op_id: Option<String>,
100}
101
102impl Default for ExecutionContextBuilder {
103    fn default() -> Self {
104        Self {
105            repo: None,
106            start_path: None,
107            principal_fallback: None,
108            fsmonitor_mode: FsMonitorMode::default(),
109            verbosity: Verbosity::Normal,
110            progress: Arc::new(NoopProgress),
111            warnings: Arc::new(NoopWarnings),
112            op_id: None,
113        }
114    }
115}
116
117impl ExecutionContextBuilder {
118    pub fn repo(mut self, repo: Repository) -> Self {
119        self.repo = Some(repo);
120        self
121    }
122
123    pub fn start_path(mut self, path: impl Into<PathBuf>) -> Self {
124        self.start_path = Some(path.into());
125        self
126    }
127
128    pub fn principal_fallback(mut self, principal: Option<(String, String)>) -> Self {
129        self.principal_fallback = principal;
130        self
131    }
132
133    pub fn fsmonitor_mode(mut self, mode: FsMonitorMode) -> Self {
134        self.fsmonitor_mode = mode;
135        self
136    }
137
138    pub fn verbosity(mut self, verbosity: Verbosity) -> Self {
139        self.verbosity = verbosity;
140        self
141    }
142
143    pub fn progress(mut self, progress: Arc<dyn ProgressSink>) -> Self {
144        self.progress = progress;
145        self
146    }
147
148    pub fn warnings(mut self, warnings: Arc<dyn WarningSink>) -> Self {
149        self.warnings = warnings;
150        self
151    }
152
153    pub fn op_id(mut self, op_id: impl Into<String>) -> Self {
154        self.op_id = Some(op_id.into());
155        self
156    }
157
158    pub fn build(self) -> ExecutionContext {
159        ExecutionContext {
160            repo: self.repo,
161            start_path: self.start_path,
162            principal_fallback: self.principal_fallback,
163            fsmonitor_mode: self.fsmonitor_mode,
164            verbosity: self.verbosity,
165            progress: self.progress,
166            warnings: self.warnings,
167            op_id: self.op_id,
168        }
169    }
170}
171
172#[cfg(test)]
173mod tests {
174    use super::*;
175
176    #[test]
177    fn default_context_has_no_repo_and_noop_sinks() {
178        let ctx = ExecutionContext::builder().build();
179
180        assert!(matches!(
181            ctx.require_repo(),
182            Err(HeddleError::RepositoryNotFound(_))
183        ));
184        assert_eq!(ctx.verbosity(), Verbosity::Normal);
185        assert!(ctx.op_id().is_none());
186        assert_eq!(ctx.fsmonitor_mode(), FsMonitorMode::Off);
187        assert!(ctx.principal_fallback().is_none());
188        ctx.progress().event(objects::ProgressEvent::Finish {
189            id: objects::TaskId(1),
190        });
191        ctx.warnings().warn(objects::Warning {
192            kind: "test".into(),
193            message: "ignored".to_string(),
194        });
195    }
196
197    #[test]
198    fn builder_sets_non_repo_fields() {
199        let ctx = ExecutionContext::builder()
200            .start_path("/tmp/heddle-verbs-context-test")
201            .principal_fallback(Some(("Luke".into(), "luke@example.com".into())))
202            .fsmonitor_mode(FsMonitorMode::Watchman)
203            .verbosity(Verbosity::Verbose)
204            .op_id("op-123")
205            .build();
206
207        assert_eq!(ctx.verbosity(), Verbosity::Verbose);
208        assert_eq!(ctx.op_id(), Some("op-123"));
209        assert_eq!(
210            ctx.start_path(),
211            Some(std::path::Path::new("/tmp/heddle-verbs-context-test"))
212        );
213        assert_eq!(ctx.principal_fallback(), Some(("Luke", "luke@example.com")));
214        assert_eq!(
215            ctx.worktree_status_options().fsmonitor.mode,
216            FsMonitorMode::Watchman
217        );
218    }
219}