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