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;
5
6use objects::HeddleError;
7use repo::{FsMonitorMode, Repository, WorktreeStatusOptions};
8
9/// Semantic execution state for embeddable Heddle operations.
10///
11/// Carries only resolved values: the caller (usually the CLI) resolves user
12/// config before construction, so this crate never reads config files.
13pub struct ExecutionContext {
14    repo: Option<Repository>,
15    start_path: Option<PathBuf>,
16    principal_fallback: Option<(String, String)>,
17    hosted_principal: Option<(String, String)>,
18    hosted_account_unclaimed: bool,
19    fsmonitor_mode: FsMonitorMode,
20}
21
22impl ExecutionContext {
23    pub fn builder() -> ExecutionContextBuilder {
24        ExecutionContextBuilder::default()
25    }
26
27    pub fn require_repo(&self) -> Result<&Repository, HeddleError> {
28        self.repo
29            .as_ref()
30            .ok_or_else(|| HeddleError::RepositoryNotFound(PathBuf::from(".")))
31    }
32
33    pub fn repo(&self) -> Option<&Repository> {
34        self.repo.as_ref()
35    }
36
37    pub fn start_path(&self) -> Option<&std::path::Path> {
38        self.start_path.as_deref()
39    }
40
41    /// User-config principal fallback as an optional `(name, email)` pair.
42    pub fn principal_fallback(&self) -> Option<(&str, &str)> {
43        self.principal_fallback
44            .as_ref()
45            .map(|(name, email)| (name.as_str(), email.as_str()))
46    }
47
48    /// Hosted-account principal derived from a local login, used only when no
49    /// local principal is configured.
50    pub fn hosted_principal(&self) -> Option<(&str, &str)> {
51        self.hosted_principal
52            .as_ref()
53            .map(|(name, email)| (name.as_str(), email.as_str()))
54    }
55
56    /// Whether local hosted state identifies an account that still needs its
57    /// human claim ceremony before it can supply accountable attribution.
58    pub fn hosted_account_unclaimed(&self) -> bool {
59        self.hosted_account_unclaimed
60    }
61
62    /// Resolved fsmonitor mode for worktree-status hot paths.
63    pub fn fsmonitor_mode(&self) -> FsMonitorMode {
64        self.fsmonitor_mode
65    }
66
67    pub fn worktree_status_options(&self) -> WorktreeStatusOptions {
68        WorktreeStatusOptions {
69            fsmonitor: repo::FsMonitorSettings {
70                mode: self.fsmonitor_mode,
71            },
72        }
73    }
74}
75
76/// Builder for [`ExecutionContext`].
77#[derive(Default)]
78pub struct ExecutionContextBuilder {
79    repo: Option<Repository>,
80    start_path: Option<PathBuf>,
81    principal_fallback: Option<(String, String)>,
82    hosted_principal: Option<(String, String)>,
83    hosted_account_unclaimed: bool,
84    fsmonitor_mode: FsMonitorMode,
85}
86
87impl ExecutionContextBuilder {
88    pub fn repo(mut self, repo: Repository) -> Self {
89        self.repo = Some(repo);
90        self
91    }
92
93    pub fn start_path(mut self, path: impl Into<PathBuf>) -> Self {
94        self.start_path = Some(path.into());
95        self
96    }
97
98    pub fn principal_fallback(mut self, principal: Option<(String, String)>) -> Self {
99        self.principal_fallback = principal;
100        self
101    }
102
103    pub fn hosted_principal(mut self, principal: Option<(String, String)>) -> Self {
104        self.hosted_principal = principal;
105        self
106    }
107
108    pub fn hosted_account_unclaimed(mut self, unclaimed: bool) -> Self {
109        self.hosted_account_unclaimed = unclaimed;
110        self
111    }
112
113    pub fn fsmonitor_mode(mut self, mode: FsMonitorMode) -> Self {
114        self.fsmonitor_mode = mode;
115        self
116    }
117
118    pub fn build(self) -> ExecutionContext {
119        ExecutionContext {
120            repo: self.repo,
121            start_path: self.start_path,
122            principal_fallback: self.principal_fallback,
123            hosted_principal: self.hosted_principal,
124            hosted_account_unclaimed: self.hosted_account_unclaimed,
125            fsmonitor_mode: self.fsmonitor_mode,
126        }
127    }
128}
129
130#[cfg(test)]
131mod tests {
132    use super::*;
133
134    #[test]
135    fn default_context_has_no_repo_and_noop_sinks() {
136        let ctx = ExecutionContext::builder().build();
137
138        assert!(matches!(
139            ctx.require_repo(),
140            Err(HeddleError::RepositoryNotFound(_))
141        ));
142        assert_eq!(ctx.fsmonitor_mode(), FsMonitorMode::Off);
143        assert!(ctx.principal_fallback().is_none());
144        assert!(!ctx.hosted_account_unclaimed());
145    }
146
147    #[test]
148    fn builder_sets_non_repo_fields() {
149        let ctx = ExecutionContext::builder()
150            .start_path("/tmp/heddle-verbs-context-test")
151            .principal_fallback(Some(("Luke".into(), "luke@example.com".into())))
152            .hosted_account_unclaimed(true)
153            .fsmonitor_mode(FsMonitorMode::Watchman)
154            .build();
155
156        assert_eq!(
157            ctx.start_path(),
158            Some(std::path::Path::new("/tmp/heddle-verbs-context-test"))
159        );
160        assert_eq!(ctx.principal_fallback(), Some(("Luke", "luke@example.com")));
161        assert!(ctx.hosted_account_unclaimed());
162        assert_eq!(
163            ctx.worktree_status_options().fsmonitor.mode,
164            FsMonitorMode::Watchman
165        );
166    }
167}