1#[macro_use]
42extern crate log;
43#[macro_use]
44extern crate lazy_static;
45
46use regex::Regex;
47use std::io::prelude::*;
48
49#[derive(Debug)]
51pub struct Submodule {
52 name: String,
53 entries: Vec<(String, String)>,
54}
55
56#[allow(dead_code)]
57impl Submodule {
58 pub fn new(name: &str, entries: Vec<(String, String)>) -> Self {
59 return Submodule {
60 name: name.to_string(),
61 entries: entries,
62 };
63 }
64
65 pub fn name(&self) -> &str {
68 &self.name
69 }
70
71 pub fn path(&self) -> Option<String> {
74 for (k, v) in &self.entries {
75 if k == "path" {
76 return Some(v.clone());
77 }
78 }
79 return None;
80 }
81
82 pub fn entries(&self) -> &Vec<(String, String)> {
85 &self.entries
86 }
87}
88
89lazy_static! {
90 static ref RE_COMMENT: Regex = Regex::new(r#"^\s*#.*"#).unwrap();
93 static ref RE_MODULE: Regex = Regex::new(r#"^\[submodule\s*"([^""]+)"\s*\]"#).unwrap();
94 static ref RE_MODULE_ENTRY: Regex = Regex::new(r#"^\s*(\S+)\s*=\s*(.*)\s*"#).unwrap();
95}
96
97pub fn read_gitmodules<R>(reader: R) -> std::io::Result<Vec<Submodule>>
100where
101 R: BufRead,
102{
103 let mut submodules: Vec<Submodule> = Vec::new();
104
105 let mut module_name: Option<String> = None;
106 let mut module_entries: Vec<(String, String)> = Vec::new();
107
108 for (n, line) in reader.lines().enumerate() {
109 let line = line.unwrap();
110 let line = line.trim();
111 if line.is_empty() {
112 continue;
113 }
114 trace!("Parsing line {}: '{}'", n, &line);
115 if RE_COMMENT.is_match(&line) {
116 continue;
117 } else if let Some(capture) = RE_MODULE.captures(&line) {
118 let submodule_name = capture.get(1).unwrap().as_str();
119 if let Some(name) = module_name.clone() {
120 let submodule = Submodule::new(&name, module_entries.clone());
121 submodules.push(submodule);
122 }
123 module_name = Some(submodule_name.to_string());
124 module_entries = Vec::new();
125 } else if let Some(capture) = RE_MODULE_ENTRY.captures(&line) {
126 let key = capture.get(1).unwrap().as_str();
127 let val = capture.get(2).unwrap().as_str();
128 module_entries.push((key.to_string(), val.to_string()));
129 } else {
130 error!("ERROR: invalid line {}: '{}'", n, line);
131 }
132 }
133
134 if let Some(name) = module_name {
135 let submodule = Submodule::new(&name, module_entries.clone());
136 submodules.push(submodule);
137 }
138
139 Ok(submodules)
140}
141
142#[cfg(test)]
143mod tests {
144 use std::io::BufReader;
145
146 use super::*;
147
148 use std::sync::{Once, ONCE_INIT};
149
150 static INIT: Once = ONCE_INIT;
151
152 fn setup() {
154 INIT.call_once(|| {
155 env_logger::init();
156 });
157 }
158
159 #[test]
160 fn gitmodules_with_comments() {
161 setup();
162
163 let text = r#"
164# this is a comment line
165[submodule "foo"]
166 path = "some/path"
167"#
168 .as_bytes();
169 let text = BufReader::new(text);
170 let submodules = read_gitmodules(text).unwrap();
171
172 assert_eq!(1, submodules.len());
173
174 let module = submodules.first().unwrap();
175 assert_eq!("foo", module.name());
176 assert_eq!("\"some/path\"", module.path().unwrap());
177 }
178
179 #[test]
180 fn gitmodules_with_broken_lines() {
181 setup();
182
183 let text = r#"
184# the next line is normally invalid because of the missing white space before the identifier
185 [submodule"foo"]
186 [submodule "bar"]
187
188path="bar/path"
189 one = 1
190 two=2
191[submodule "baz"]
192 path = "baz/path"
193 flag = true
194"#
195 .as_bytes();
196 let text = BufReader::new(text);
197 let submodules = read_gitmodules(text).unwrap();
198
199 assert_eq!(3, submodules.len());
200
201 let module = submodules.first().unwrap();
202 assert_eq!("foo", module.name());
203 assert!(module.entries().is_empty());
204
205 let module = submodules.get(1).unwrap();
206 assert_eq!("bar", module.name());
207 assert_eq!("\"bar/path\"", module.path().unwrap());
208 let actual_one = module.entries().iter().find(|&(key, _)| key == "one");
209 let expected_one = ("one".to_string(), "1".to_string());
210 assert_eq!(Some(&expected_one), actual_one);
211 let actual_two = module.entries().iter().find(|&(key, _)| key == "two");
212 let expected_two = ("two".to_string(), "2".to_string());
213 assert_eq!(Some(&expected_two), actual_two);
214
215 let module = submodules.get(2).unwrap();
216 assert_eq!("baz", module.name());
217 assert_eq!("\"baz/path\"", module.path().unwrap());
218 let actual = module.entries().iter().find(|&(key, _)| key == "flag");
219 let expected = ("flag".to_string(), "true".to_string());
220 assert_eq!(Some(&expected), actual);
221 }
222}