#[macro_use]
extern crate log;
#[macro_use]
extern crate lazy_static;
use regex::Regex;
use std::io::prelude::*;
#[derive(Debug)]
pub struct Submodule {
name: String,
entries: Vec<(String, String)>,
}
#[allow(dead_code)]
impl Submodule {
pub fn new(name: &str, entries: Vec<(String, String)>) -> Self {
return Submodule {
name: name.to_string(),
entries: entries,
};
}
pub fn name(&self) -> &str {
&self.name
}
pub fn path(&self) -> Option<String> {
for (k, v) in &self.entries {
if k == "path" {
return Some(v.clone());
}
}
return None;
}
pub fn entries(&self) -> &Vec<(String, String)> {
&self.entries
}
}
lazy_static! {
static ref RE_COMMENT: Regex = Regex::new(r#"^\s*#.*"#).unwrap();
static ref RE_MODULE: Regex = Regex::new(r#"^\[submodule\s*"([^""]+)"\s*\]"#).unwrap();
static ref RE_MODULE_ENTRY: Regex = Regex::new(r#"^\s*(\S+)\s*=\s*(.*)\s*"#).unwrap();
}
pub fn read_gitmodules<R>(reader: R) -> std::io::Result<Vec<Submodule>>
where
R: BufRead,
{
let mut submodules: Vec<Submodule> = Vec::new();
let mut module_name: Option<String> = None;
let mut module_entries: Vec<(String, String)> = Vec::new();
for (n, line) in reader.lines().enumerate() {
let line = line.unwrap();
let line = line.trim();
if line.is_empty() {
continue;
}
trace!("Parsing line {}: '{}'", n, &line);
if RE_COMMENT.is_match(&line) {
continue;
} else if let Some(capture) = RE_MODULE.captures(&line) {
let submodule_name = capture.get(1).unwrap().as_str();
if let Some(name) = module_name.clone() {
let submodule = Submodule::new(&name, module_entries.clone());
submodules.push(submodule);
}
module_name = Some(submodule_name.to_string());
module_entries = Vec::new();
} else if let Some(capture) = RE_MODULE_ENTRY.captures(&line) {
let key = capture.get(1).unwrap().as_str();
let val = capture.get(2).unwrap().as_str();
module_entries.push((key.to_string(), val.to_string()));
} else {
error!("ERROR: invalid line {}: '{}'", n, line);
}
}
if let Some(name) = module_name {
let submodule = Submodule::new(&name, module_entries.clone());
submodules.push(submodule);
}
Ok(submodules)
}
#[cfg(test)]
mod tests {
use std::io::BufReader;
use super::*;
use std::sync::{Once, ONCE_INIT};
static INIT: Once = ONCE_INIT;
fn setup() {
INIT.call_once(|| {
env_logger::init();
});
}
#[test]
fn gitmodules_with_comments() {
setup();
let text = r#"
# this is a comment line
[submodule "foo"]
path = "some/path"
"#
.as_bytes();
let text = BufReader::new(text);
let submodules = read_gitmodules(text).unwrap();
assert_eq!(1, submodules.len());
let module = submodules.first().unwrap();
assert_eq!("foo", module.name());
assert_eq!("\"some/path\"", module.path().unwrap());
}
#[test]
fn gitmodules_with_broken_lines() {
setup();
let text = r#"
# the next line is normally invalid because of the missing white space before the identifier
[submodule"foo"]
[submodule "bar"]
path="bar/path"
one = 1
two=2
[submodule "baz"]
path = "baz/path"
flag = true
"#
.as_bytes();
let text = BufReader::new(text);
let submodules = read_gitmodules(text).unwrap();
assert_eq!(3, submodules.len());
let module = submodules.first().unwrap();
assert_eq!("foo", module.name());
assert!(module.entries().is_empty());
let module = submodules.get(1).unwrap();
assert_eq!("bar", module.name());
assert_eq!("\"bar/path\"", module.path().unwrap());
let actual_one = module.entries().iter().find(|&(key, _)| key == "one");
let expected_one = ("one".to_string(), "1".to_string());
assert_eq!(Some(&expected_one), actual_one);
let actual_two = module.entries().iter().find(|&(key, _)| key == "two");
let expected_two = ("two".to_string(), "2".to_string());
assert_eq!(Some(&expected_two), actual_two);
let module = submodules.get(2).unwrap();
assert_eq!("baz", module.name());
assert_eq!("\"baz/path\"", module.path().unwrap());
let actual = module.entries().iter().find(|&(key, _)| key == "flag");
let expected = ("flag".to_string(), "true".to_string());
assert_eq!(Some(&expected), actual);
}
}