gitmodules/
lib.rs

1// Copyright 2019 Marcus Geiger
2//
3// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
4// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
5// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
6// option. This file may not be copied, modified, or distributed
7// except according to those terms.
8
9/*!
10This crate provides a simple regular expression based parsing library
11for reading the `.gitmodules` file of a Git repository.
12
13# Usage
14
15Add this to your `Cargo.toml`:
16
17``` toml
18[dependencies]
19gitmodules = "0.1"
20```
21
22Usage is trivial:
23``` rust
24use std::io::BufReader;
25use gitmodules::{read_gitmodules, Submodule};
26
27fn demo() {
28    let text = r#"
29# this is a comment line
30[submodule "foo"]
31    path = "some/path"
32"#
33    .as_bytes();
34    let text = BufReader::new(text);
35    let submodules = read_gitmodules(text).unwrap();
36    println!("Submodule name {}", submodules.first().unwrap().name());
37}
38```
39*/
40
41#[macro_use]
42extern crate log;
43#[macro_use]
44extern crate lazy_static;
45
46use regex::Regex;
47use std::io::prelude::*;
48
49/// Represents a Git submodule entry with its attributes.
50#[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    /// Returns the name of the submodule. The name is the only
66    /// required attribute of the submodule.
67    pub fn name(&self) -> &str {
68        &self.name
69    }
70
71    /// Returns the optional path of the Git submodule in the Git
72    /// repository.
73    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    /// Returns the optional entries of the Git submodule, excluding
83    /// its name.
84    pub fn entries(&self) -> &Vec<(String, String)> {
85        &self.entries
86    }
87}
88
89lazy_static! {
90    // Note: be lenient about white spaces, although a proper
91    // gitmodules file looks different.
92    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
97/// Read a `.gitmodules` file and return a vector of the configured
98/// Git submodules.
99pub 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    /// Setup function that is only run once, even if called multiple times.
153    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}