Skip to main content

verbs/
init_plan.rs

1// SPDX-License-Identifier: Apache-2.0
2//! Pure `heddle init` planning: principal status, side-effects list, paths.
3
4use std::path::{Path, PathBuf};
5
6use objects::object::Principal;
7
8use crate::principal_lacks_accountable_identity;
9
10/// Recommended command when no principal is configured.
11pub const SET_PRINCIPAL_COMMAND: &str =
12    "heddle init --principal-name <name> --principal-email <email>";
13
14/// Pure principal configuration status for init output.
15#[derive(Debug, Clone, PartialEq, Eq)]
16pub struct InitPrincipalPlan {
17    pub status: &'static str,
18    pub source: Option<&'static str>,
19    pub name: Option<String>,
20    pub email: Option<String>,
21    pub recommended_action: Option<&'static str>,
22}
23
24impl InitPrincipalPlan {
25    pub fn configured(source: &'static str, principal: &Principal) -> Self {
26        Self {
27            status: "configured",
28            source: Some(source),
29            name: Some(principal.name_lossy().into_owned()),
30            email: Some(principal.email_lossy().into_owned()),
31            recommended_action: None,
32        }
33    }
34
35    pub fn not_configured() -> Self {
36        Self {
37            status: "not_configured",
38            source: None,
39            name: None,
40            email: None,
41            recommended_action: Some(SET_PRINCIPAL_COMMAND),
42        }
43    }
44}
45
46/// Whether a principal lacks accountable identity (same policy as capture).
47pub fn principal_is_unconfigured(principal: &Principal) -> bool {
48    principal_lacks_accountable_identity(&principal.name_lossy(), &principal.email_lossy())
49}
50
51/// Prefer the first configured principal among ordered candidates.
52///
53/// Each entry is `(source_label, principal)`. Callers gather env/repo/git/user
54/// facts and pass them in precedence order.
55pub fn select_init_principal(candidates: &[(&'static str, Principal)]) -> InitPrincipalPlan {
56    for (source, principal) in candidates {
57        if !principal_is_unconfigured(principal) {
58            return InitPrincipalPlan::configured(source, principal);
59        }
60    }
61    InitPrincipalPlan::not_configured()
62}
63
64/// Side-effect lines for init human/JSON output.
65pub fn init_side_effects(has_git: bool, principal_configured: bool) -> Vec<String> {
66    let mut side_effects = Vec::new();
67    if has_git {
68        side_effects.push("created Heddle sidecar for the existing Git repository".to_string());
69        side_effects.push("updated .git/info/exclude for Heddle metadata".to_string());
70        side_effects.push("left Git-tracked files untouched".to_string());
71    } else {
72        side_effects.push("created Heddle repository metadata".to_string());
73    }
74    if principal_configured {
75        side_effects.push("updated default principal attribution".to_string());
76    }
77    side_effects
78}
79
80/// Resolve a path against a known current directory (no ambient cwd read).
81pub fn resolve_absolute_path(cwd: &Path, path: &Path) -> PathBuf {
82    if path.is_absolute() {
83        path.to_path_buf()
84    } else {
85        cwd.join(path)
86    }
87}
88
89/// Default next action after a successful init.
90pub fn init_recommended_action() -> &'static str {
91    "heddle capture -m \"...\""
92}
93
94#[cfg(test)]
95mod tests {
96    use super::*;
97
98    #[test]
99    fn principal_selection_and_side_effects() {
100        let unknown = Principal::new("Unknown", "unknown@example.com");
101        let ada = Principal::new("Ada", "ada@example.com");
102        let plan = select_init_principal(&[("environment", unknown), ("user_config", ada)]);
103        assert_eq!(plan.status, "configured");
104        assert_eq!(plan.source, Some("user_config"));
105        assert_eq!(plan.name.as_deref(), Some("Ada"));
106
107        let empty = select_init_principal(&[]);
108        assert_eq!(empty.status, "not_configured");
109        assert_eq!(empty.recommended_action, Some(SET_PRINCIPAL_COMMAND));
110
111        let se = init_side_effects(true, true);
112        assert!(se.iter().any(|s| s.contains("sidecar")));
113        assert!(se.iter().any(|s| s.contains("principal")));
114
115        assert_eq!(
116            resolve_absolute_path(Path::new("/cwd"), Path::new("rel")),
117            PathBuf::from("/cwd/rel")
118        );
119        assert_eq!(
120            resolve_absolute_path(Path::new("/cwd"), Path::new("/abs")),
121            PathBuf::from("/abs")
122        );
123    }
124}