features_cli/
file_scanner.rs1use anyhow::{Context, Result};
2use std::fs;
3use std::path::Path;
4
5use crate::git_helper::get_commits_for_path;
6use crate::models::Feature;
7use crate::readme_parser::read_readme_info;
8
9fn is_documentation_directory(dir_path: &Path) -> bool {
10 let dir_name = dir_path
11 .file_name()
12 .and_then(|name| name.to_str())
13 .unwrap_or("");
14
15 let doc_dirs = ["docs", "__docs__", ".docs"];
17
18 doc_dirs.contains(&dir_name.to_lowercase().as_str())
19}
20
21fn is_inside_documentation_directory(dir_path: &Path) -> bool {
22 for ancestor in dir_path.ancestors().skip(1) {
24 if is_documentation_directory(ancestor) {
25 return true;
26 }
27 }
28 false
29}
30
31fn is_direct_subfolder_of_features(dir_path: &Path) -> bool {
32 if let Some(parent) = dir_path.parent()
33 && let Some(parent_name) = parent.file_name().and_then(|name| name.to_str())
34 {
35 return parent_name == "features";
36 }
37 false
38}
39
40fn find_readme_file(dir_path: &Path) -> Option<std::path::PathBuf> {
41 let readme_candidates = ["README.md", "README.mdx"];
42
43 for candidate in &readme_candidates {
44 let readme_path = dir_path.join(candidate);
45 if readme_path.exists() {
46 return Some(readme_path);
47 }
48 }
49
50 None
51}
52
53pub fn list_files_recursive(dir: &Path) -> Result<Vec<Feature>> {
54 list_files_recursive_impl(dir, false)
55}
56
57pub fn list_files_recursive_with_changes(dir: &Path) -> Result<Vec<Feature>> {
58 list_files_recursive_impl(dir, true)
59}
60
61fn read_decision_files(feature_path: &Path) -> Result<Vec<String>> {
62 let mut decisions = Vec::new();
63
64 let decision_paths = [
66 feature_path.join(".docs").join("decisions"),
67 feature_path.join("__docs__").join("decisions"),
68 ];
69
70 for decisions_dir in &decision_paths {
71 if decisions_dir.exists() && decisions_dir.is_dir() {
72 let entries = fs::read_dir(decisions_dir).with_context(|| {
73 format!(
74 "could not read decisions directory `{}`",
75 decisions_dir.display()
76 )
77 })?;
78
79 for entry in entries {
80 let entry = entry?;
81 let path = entry.path();
82
83 if path.is_file()
85 && let Some(file_name) = path.file_name()
86 {
87 let file_name_str = file_name.to_string_lossy();
88 if file_name_str.ends_with(".md") && file_name_str != "README.md" {
89 let content = fs::read_to_string(&path).with_context(|| {
90 format!("could not read decision file `{}`", path.display())
91 })?;
92 decisions.push(content);
93 }
94 }
95 }
96 break; }
98 }
99
100 Ok(decisions)
101}
102
103fn list_files_recursive_impl(dir: &Path, include_changes: bool) -> Result<Vec<Feature>> {
104 let entries = fs::read_dir(dir)
105 .with_context(|| format!("could not read directory `{}`", dir.display()))?;
106
107 let mut entries: Vec<_> = entries.collect::<Result<_, _>>()?;
108 entries.sort_by_key(|entry| entry.path());
109
110 let mut features: Vec<Feature> = Vec::new();
111
112 for entry in entries {
113 let path = entry.path();
114 let name = path.file_name().unwrap().to_string_lossy();
115
116 if path.is_dir() {
117 if !is_documentation_directory(&path)
120 && !is_inside_documentation_directory(&path)
121 && is_direct_subfolder_of_features(&path)
122 {
123 let (owner, description, meta) = if let Some(readme_path) = find_readme_file(&path)
125 {
126 read_readme_info(&readme_path)?
127 } else {
128 (
129 "Unknown".to_string(),
130 "".to_string(),
131 std::collections::HashMap::new(),
132 )
133 };
134
135 let changes = if include_changes {
136 get_commits_for_path(&path, &path.to_string_lossy()).unwrap_or_default()
137 } else {
138 Vec::new()
139 };
140
141 let decisions = read_decision_files(&path).unwrap_or_default();
143
144 let nested_features_path = path.join("features");
146 let nested_features =
147 if nested_features_path.exists() && nested_features_path.is_dir() {
148 list_files_recursive_impl(&nested_features_path, include_changes)
149 .unwrap_or_default()
150 } else {
151 Vec::new()
152 };
153
154 features.push(Feature {
155 name: name.to_string(),
156 description,
157 owner,
158 path: path.to_string_lossy().to_string(),
159 features: nested_features,
160 meta,
161 changes,
162 decisions,
163 });
164 } else {
165 let new_features = list_files_recursive_impl(&path, include_changes);
166 features.extend(new_features?);
167 }
168 }
169 }
170
171 Ok(features)
172}