1use crate::types::{DocDiffFile, DocDiffFileTreeNode};
8
9pub fn build_file_tree(files: &[DocDiffFile]) -> Vec<DocDiffFileTreeNode> {
11 let mut sorted: Vec<&DocDiffFile> = files.iter().collect();
12 sorted.sort_by(|a, b| a.path.cmp(&b.path));
14
15 let mut roots: Vec<DocDiffFileTreeNode> = Vec::new();
16
17 for file in sorted {
18 let segments: Vec<&str> = file.path.split('/').filter(|s| !s.is_empty()).collect();
19 let display: &[&str] = if segments.first() == Some(&"docs") {
20 &segments[1..]
21 } else {
22 &segments[..]
23 };
24 let file_name = display.last().copied().unwrap_or(file.path.as_str());
25 let group_segments = if display.is_empty() {
26 &[][..]
27 } else {
28 &display[..display.len() - 1]
29 };
30
31 let mut cursor = &mut roots;
32 let mut id_prefix = String::from("docs");
33
34 for segment in group_segments {
35 id_prefix = format!("{id_prefix}/{segment}");
36 let pos = cursor.iter().position(
38 |node| matches!(node, DocDiffFileTreeNode::Group { id, .. } if id == &id_prefix),
39 );
40 let idx = match pos {
41 Some(i) => i,
42 None => {
43 cursor.push(DocDiffFileTreeNode::Group {
44 id: id_prefix.clone(),
45 label: (*segment).to_string(),
46 children: Vec::new(),
47 });
48 cursor.len() - 1
49 }
50 };
51 cursor = match &mut cursor[idx] {
52 DocDiffFileTreeNode::Group { children, .. } => children,
53 _ => unreachable!("group node selected by id is always a group"),
54 };
55 }
56
57 cursor.push(DocDiffFileTreeNode::File {
58 id: file.path.clone(),
59 label: file_name.to_string(),
60 path: file.path.clone(),
61 old_path: file.old_path.clone(),
62 status: file.status,
63 added_lines: file.added_lines,
64 removed_lines: file.removed_lines,
65 });
66 }
67
68 roots
69}
70
71#[cfg(test)]
72mod tests {
73 use super::*;
74 use crate::types::DocDiffFileStatus;
75
76 fn file(path: &str) -> DocDiffFile {
77 DocDiffFile {
78 path: path.into(),
79 old_path: None,
80 status: DocDiffFileStatus::Modified,
81 added_lines: 1,
82 removed_lines: 2,
83 hunks: vec![],
84 blocks: None,
85 }
86 }
87
88 #[test]
89 fn groups_changed_files_by_docs_path_segments() {
90 let tree = build_file_tree(&[
91 file("docs/dev/client.svx"),
92 file("docs/game-design/world.md"),
93 ]);
94 assert_eq!(
95 tree,
96 vec![
97 DocDiffFileTreeNode::Group {
98 id: "docs/dev".into(),
99 label: "dev".into(),
100 children: vec![DocDiffFileTreeNode::File {
101 id: "docs/dev/client.svx".into(),
102 label: "client.svx".into(),
103 path: "docs/dev/client.svx".into(),
104 old_path: None,
105 status: DocDiffFileStatus::Modified,
106 added_lines: 1,
107 removed_lines: 2,
108 }],
109 },
110 DocDiffFileTreeNode::Group {
111 id: "docs/game-design".into(),
112 label: "game-design".into(),
113 children: vec![DocDiffFileTreeNode::File {
114 id: "docs/game-design/world.md".into(),
115 label: "world.md".into(),
116 path: "docs/game-design/world.md".into(),
117 old_path: None,
118 status: DocDiffFileStatus::Modified,
119 added_lines: 1,
120 removed_lines: 2,
121 }],
122 },
123 ]
124 );
125 }
126}