ito_core/
harness_context.rs1use crate::errors::CoreResult;
14use ito_common::id::{parse_change_id, parse_module_id};
15use std::path::{Component, Path};
16
17#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
19#[serde(rename_all = "snake_case")]
20pub enum InferredItoTargetKind {
21 Change,
23 Module,
25}
26
27#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
29pub struct InferredItoTarget {
30 pub kind: InferredItoTargetKind,
32 pub id: String,
34}
35
36#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
38pub struct HarnessContextInference {
39 pub target: Option<InferredItoTarget>,
41 pub nudge: String,
43}
44
45pub fn infer_context_from_cwd(cwd: &Path) -> CoreResult<HarnessContextInference> {
52 let target = infer_target_from_path(cwd).or_else(|| infer_target_from_git_branch(cwd));
53 let nudge = nudge_for_target(target.as_ref());
54 Ok(HarnessContextInference { target, nudge })
55}
56
57fn nudge_for_target(target: Option<&InferredItoTarget>) -> String {
58 let Some(target) = target else {
59 return "No Ito change/module inferred. Re-establish the target (try: `ito list --ready`, then `ito tasks next <change-id>`).".to_string();
60 };
61
62 match target.kind {
63 InferredItoTargetKind::Change => format!(
64 "If you are still implementing change {id}, continue now: `ito tasks next {id}` (then `ito tasks start/complete` as you progress).",
65 id = target.id
66 ),
67 InferredItoTargetKind::Module => format!(
68 "If you are still working in module {id}, continue now: `ito show module {id}` then run `ito tasks next <change-id>` for the next ready change.",
69 id = target.id
70 ),
71 }
72}
73
74fn infer_target_from_path(cwd: &Path) -> Option<InferredItoTarget> {
75 let mut change: Option<String> = None;
76 let mut module: Option<String> = None;
77
78 let mut last_was_ito_dir = false;
79 let mut expect_module_dir = false;
80
81 for component in cwd.components() {
82 let Component::Normal(seg) = component else {
83 continue;
84 };
85 let Some(seg) = seg.to_str() else {
86 last_was_ito_dir = false;
87 expect_module_dir = false;
88 continue;
89 };
90
91 if expect_module_dir {
92 if let Ok(parsed) = parse_module_id(seg) {
93 module = Some(parsed.module_id.as_str().to_string());
94 }
95 expect_module_dir = false;
96 }
97
98 if let Ok(parsed) = parse_change_id(seg) {
99 change = Some(parsed.canonical.as_str().to_string());
100 }
101
102 if last_was_ito_dir && seg == "modules" {
103 expect_module_dir = true;
104 }
105
106 last_was_ito_dir = seg == ".ito";
107 }
108
109 if let Some(id) = change {
110 return Some(InferredItoTarget {
111 kind: InferredItoTargetKind::Change,
112 id,
113 });
114 }
115
116 let id = module?;
117 Some(InferredItoTarget {
118 kind: InferredItoTargetKind::Module,
119 id,
120 })
121}
122
123fn infer_target_from_git_branch(cwd: &Path) -> Option<InferredItoTarget> {
124 let branch = git_output(cwd, &["branch", "--show-current"])?
125 .trim()
126 .to_string();
127 if branch.is_empty() {
128 return None;
129 }
130
131 if let Ok(parsed) = parse_change_id(&branch) {
132 return Some(InferredItoTarget {
133 kind: InferredItoTargetKind::Change,
134 id: parsed.canonical.as_str().to_string(),
135 });
136 }
137
138 if let Ok(parsed) = parse_module_id(&branch) {
139 return Some(InferredItoTarget {
140 kind: InferredItoTargetKind::Module,
141 id: parsed.module_id.as_str().to_string(),
142 });
143 }
144
145 None
146}
147
148fn git_output(cwd: &Path, args: &[&str]) -> Option<String> {
149 let mut command = std::process::Command::new("git");
150 command.args(args).current_dir(cwd);
151
152 for (k, _) in std::env::vars_os() {
154 let k = k.to_string_lossy();
155 if k.starts_with("GIT_") {
156 command.env_remove(k.as_ref());
157 }
158 }
159
160 let output = command.output().ok()?;
161 if !output.status.success() {
162 return None;
163 }
164 Some(String::from_utf8_lossy(&output.stdout).to_string())
165}