docgen_diff/
git_parsing.rs1use crate::types::DocDiffFileStatus;
8
9#[derive(Debug, Clone, PartialEq, Eq)]
10pub struct NameStatusEntry {
11 pub status: DocDiffFileStatus,
12 pub path: String,
13 pub old_path: Option<String>,
14}
15
16pub fn parse_name_status(stdout: &str) -> Vec<NameStatusEntry> {
18 stdout
19 .split('\n')
20 .map(|line| line.trim())
21 .filter(|line| !line.is_empty())
22 .flat_map(parse_name_status_line)
23 .collect()
24}
25
26pub fn parse_untracked_docs(stdout: &str) -> Vec<String> {
28 stdout
29 .split('\n')
30 .map(|line| line.trim())
31 .filter(|line| is_doc_path(line))
32 .map(|line| line.to_string())
33 .collect()
34}
35
36fn parse_name_status_line(line: &str) -> Vec<NameStatusEntry> {
37 let mut parts = line.split('\t');
38 let raw_status = parts.next().unwrap_or("");
39 let first_path = parts.next();
40 let second_path = parts.next();
41
42 match raw_status {
43 "A" => entry(DocDiffFileStatus::Added, first_path),
44 "M" => entry(DocDiffFileStatus::Modified, first_path),
45 "D" => entry(DocDiffFileStatus::Deleted, first_path),
46 s if s.starts_with('R') => {
47 let old_is_doc = is_doc_path_opt(first_path);
48 let new_is_doc = is_doc_path_opt(second_path);
49
50 if old_is_doc && new_is_doc {
51 return vec![NameStatusEntry {
52 status: DocDiffFileStatus::Renamed,
53 old_path: Some(first_path.unwrap().to_string()),
54 path: second_path.unwrap().to_string(),
55 }];
56 }
57 if old_is_doc {
58 return vec![NameStatusEntry {
59 status: DocDiffFileStatus::Deleted,
60 path: first_path.unwrap().to_string(),
61 old_path: None,
62 }];
63 }
64 if new_is_doc {
65 return vec![NameStatusEntry {
66 status: DocDiffFileStatus::Added,
67 path: second_path.unwrap().to_string(),
68 old_path: None,
69 }];
70 }
71 vec![]
72 }
73 _ => vec![],
74 }
75}
76
77fn entry(status: DocDiffFileStatus, path: Option<&str>) -> Vec<NameStatusEntry> {
78 match path {
79 Some(p) if is_doc_path(p) => vec![NameStatusEntry {
80 status,
81 path: p.to_string(),
82 old_path: None,
83 }],
84 _ => vec![],
85 }
86}
87
88fn is_doc_path_opt(path: Option<&str>) -> bool {
89 path.map(is_doc_path).unwrap_or(false)
90}
91
92fn is_doc_path(path: &str) -> bool {
93 path.starts_with("docs/") && (path.ends_with(".md") || path.ends_with(".svx"))
94}
95
96#[cfg(test)]
97mod tests {
98 use super::*;
99 use DocDiffFileStatus::*;
100
101 fn ns(status: DocDiffFileStatus, path: &str, old_path: Option<&str>) -> NameStatusEntry {
102 NameStatusEntry {
103 status,
104 path: path.into(),
105 old_path: old_path.map(|s| s.into()),
106 }
107 }
108
109 #[test]
110 fn parses_added_modified_deleted_renamed() {
111 assert_eq!(
112 parse_name_status(
113 "A\tdocs/new.md\nM\tdocs/a.md\nD\tdocs/old.md\nR100\tdocs/from.md\tdocs/to.md\n"
114 ),
115 vec![
116 ns(Added, "docs/new.md", None),
117 ns(Modified, "docs/a.md", None),
118 ns(Deleted, "docs/old.md", None),
119 ns(Renamed, "docs/to.md", Some("docs/from.md")),
120 ]
121 );
122 }
123
124 #[test]
125 fn one_sided_docs_renames_map_to_add_or_delete() {
126 assert_eq!(
127 parse_name_status(
128 "R100\tdocs/from.md\toutside/from.md\nR100\toutside/to.md\tdocs/to.md\n"
129 ),
130 vec![
131 ns(Deleted, "docs/from.md", None),
132 ns(Added, "docs/to.md", None),
133 ]
134 );
135 }
136
137 #[test]
138 fn untracked_keeps_docs_md_and_svx() {
139 assert_eq!(
140 parse_untracked_docs("docs/a.md\ndocs/b.svx\nclient/nope.md\n"),
141 vec!["docs/a.md", "docs/b.svx"]
142 );
143 }
144}