From f10afb789497125673cbca59852ff8e12ca62eb2 Mon Sep 17 00:00:00 2001 From: Edward Shen Date: Sun, 21 Feb 2021 21:16:08 -0500 Subject: [PATCH] gitconfig writing to string --- src/config.rs | 178 ++++++++++++++++++++++++++---- src/parser.rs | 95 ++++++++++++---- tests/parser_integration_tests.rs | 22 ++++ 3 files changed, 254 insertions(+), 41 deletions(-) diff --git a/src/config.rs b/src/config.rs index 1a09a53..7c9a3f0 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,7 +1,7 @@ -use crate::parser::{parse_from_str, Event, Parser, ParserError}; +use crate::parser::{parse_from_str, Event, ParsedSectionHeader, Parser, ParserError}; use serde::Serialize; -use std::borrow::Cow; use std::collections::{HashMap, VecDeque}; +use std::{borrow::Cow, fmt::Display}; #[derive(Debug, PartialEq, Eq)] pub enum GitConfigError<'a> { @@ -23,7 +23,7 @@ pub struct GitConfig<'a> { /// SectionId to section mapping. The value of this HashMap contains actual /// events sections: HashMap>>, - section_header_separators: HashMap>>, + section_headers: HashMap>, section_id_counter: usize, section_order: VecDeque, } @@ -60,7 +60,7 @@ impl<'a> GitConfig<'a> { front_matter_events: vec![], sections: HashMap::new(), section_lookup_tree: HashMap::new(), - section_header_separators: HashMap::new(), + section_headers: HashMap::new(), section_id_counter: 0, section_order: VecDeque::new(), }; @@ -80,16 +80,16 @@ impl<'a> GitConfig<'a> { ); // Initialize new section - let (name, subname) = (header.name, header.subsection_name); - maybe_section = Some(vec![]); - current_section_name = Some(name); - current_subsection_name = subname; // We need to store the new, current id counter, so don't // use new_section_id here and use the already incremented // section id value. new_self - .section_header_separators - .insert(SectionId(new_self.section_id_counter), header.separator); + .section_headers + .insert(SectionId(new_self.section_id_counter), header.clone()); + let (name, subname) = (header.name, header.subsection_name); + maybe_section = Some(vec![]); + current_section_name = Some(name); + current_subsection_name = subname; } e @ Event::Key(_) | e @ Event::Value(_) @@ -513,6 +513,62 @@ impl<'a> GitConfig<'a> { *value = new_value.into(); Ok(()) } + + /// Sets a multivar in a given section, optional subsection, and key value. + /// + /// This internally zips together the new values and the existing values. + /// As a result, if more new values are provided than the current amount of + /// multivars, then the latter values are not applied. If there are less + /// new values than old ones then the remaining old values are unmodified. + /// + /// If you need finer control over which values of the multivar are set, + /// consider using [`get_raw_multi_value_mut`]. + /// + /// todo: examples and errors + /// + /// [`get_raw_multi_value_mut`]: Self::get_raw_multi_value_mut + pub fn set_raw_multi_value<'b>( + &mut self, + section_name: &'b str, + subsection_name: Option<&'b str>, + key: &'b str, + new_values: Vec>, + ) -> Result<(), GitConfigError<'b>> { + let values = self.get_raw_multi_value_mut(section_name, subsection_name, key)?; + for (old, new) in values.into_iter().zip(new_values) { + *old = new; + } + Ok(()) + } +} + +impl Display for GitConfig<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + for front_matter in &self.front_matter_events { + front_matter.fmt(f)?; + } + + for section_id in &self.section_order { + self.section_headers.get(section_id).unwrap().fmt(f)?; + let mut found_key = false; + for event in self.sections.get(section_id).unwrap() { + match event { + Event::Key(k) => { + found_key = true; + k.fmt(f)?; + } + Event::Whitespace(w) if found_key => { + found_key = false; + w.fmt(f)?; + write!(f, "=")?; + } + e => e.fmt(f)?, + } + } + } + + Ok(()) + } } #[cfg(test)] @@ -522,7 +578,7 @@ mod from_parser { #[test] fn parse_empty() { let config = GitConfig::from_str("").unwrap(); - assert!(config.section_header_separators.is_empty()); + assert!(config.section_headers.is_empty()); assert_eq!(config.section_id_counter, 0); assert!(config.section_lookup_tree.is_empty()); assert!(config.sections.is_empty()); @@ -534,10 +590,17 @@ mod from_parser { let mut config = GitConfig::from_str("[core]\na=b\nc=d").unwrap(); let expected_separators = { let mut map = HashMap::new(); - map.insert(SectionId(0), None); + map.insert( + SectionId(0), + ParsedSectionHeader { + name: "core".into(), + separator: None, + subsection_name: None, + }, + ); map }; - assert_eq!(config.section_header_separators, expected_separators); + assert_eq!(config.section_headers, expected_separators); assert_eq!(config.section_id_counter, 1); let expected_lookup_tree = { let mut tree = HashMap::new(); @@ -572,10 +635,17 @@ mod from_parser { let mut config = GitConfig::from_str("[core.subsec]\na=b\nc=d").unwrap(); let expected_separators = { let mut map = HashMap::new(); - map.insert(SectionId(0), Some(Cow::Borrowed("."))); + map.insert( + SectionId(0), + ParsedSectionHeader { + name: "core".into(), + separator: Some(".".into()), + subsection_name: Some("subsec".into()), + }, + ); map }; - assert_eq!(config.section_header_separators, expected_separators); + assert_eq!(config.section_headers, expected_separators); assert_eq!(config.section_id_counter, 1); let expected_lookup_tree = { let mut tree = HashMap::new(); @@ -612,11 +682,25 @@ mod from_parser { let mut config = GitConfig::from_str("[core]\na=b\nc=d\n[other]e=f").unwrap(); let expected_separators = { let mut map = HashMap::new(); - map.insert(SectionId(0), None); - map.insert(SectionId(1), None); + map.insert( + SectionId(0), + ParsedSectionHeader { + name: "core".into(), + separator: None, + subsection_name: None, + }, + ); + map.insert( + SectionId(1), + ParsedSectionHeader { + name: "other".into(), + separator: None, + subsection_name: None, + }, + ); map }; - assert_eq!(config.section_header_separators, expected_separators); + assert_eq!(config.section_headers, expected_separators); assert_eq!(config.section_id_counter, 2); let expected_lookup_tree = { let mut tree = HashMap::new(); @@ -666,11 +750,25 @@ mod from_parser { let mut config = GitConfig::from_str("[core]\na=b\nc=d\n[core]e=f").unwrap(); let expected_separators = { let mut map = HashMap::new(); - map.insert(SectionId(0), None); - map.insert(SectionId(1), None); + map.insert( + SectionId(0), + ParsedSectionHeader { + name: "core".into(), + separator: None, + subsection_name: None, + }, + ); + map.insert( + SectionId(1), + ParsedSectionHeader { + name: "core".into(), + separator: None, + subsection_name: None, + }, + ); map }; - assert_eq!(config.section_header_separators, expected_separators); + assert_eq!(config.section_headers, expected_separators); assert_eq!(config.section_id_counter, 2); let expected_lookup_tree = { let mut tree = HashMap::new(); @@ -868,3 +966,41 @@ mod get_raw_multi_value { ); } } + +#[cfg(test)] +mod display { + use super::*; + + #[test] + fn can_reconstruct_empty_config() { + let config = r#" + + "#; + assert_eq!(GitConfig::from_str(config).unwrap().to_string(), config); + } + + #[test] + fn can_reconstruct_non_empty_config() { + let config = r#"[user] + email = code@eddie.sh + name = Edward Shen +[core] + autocrlf = input +[push] + default = simple +[commit] + gpgsign = true +[gpg] + program = gpg +[url "ssh://git@github.com/"] + insteadOf = "github://" +[url "ssh://git@git.eddie.sh/edward/"] + insteadOf = "gitea://" +[pull] + ff = only +[init] + defaultBranch = master"#; + + assert_eq!(GitConfig::from_str(config).unwrap().to_string(), config); + } +} diff --git a/src/parser.rs b/src/parser.rs index 37267bd..f221d97 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -17,7 +17,9 @@ use nom::error::{Error as NomError, ErrorKind}; use nom::sequence::delimited; use nom::IResult; use nom::{branch::alt, multi::many0}; -use std::{borrow::Cow, iter::FusedIterator}; +use std::borrow::{Borrow, Cow}; +use std::fmt::Display; +use std::iter::FusedIterator; /// Syntactic events that occurs in the config. #[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)] @@ -38,7 +40,8 @@ pub enum Event<'a> { Value(Cow<'a, str>), /// Represents any token used to signify a new line character. On Unix /// platforms, this is typically just `\n`, but can be any valid newline - /// sequence. + /// sequence. Multiple newlines (such as `\n\n`) will be merged as a single + /// newline event. Newline(Cow<'a, str>), /// Any value that isn't completed. This occurs when the value is continued /// onto the next line. A Newline event is guaranteed after, followed by @@ -51,6 +54,21 @@ pub enum Event<'a> { Whitespace(Cow<'a, str>), } +impl Display for Event<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Comment(e) => e.fmt(f), + Self::SectionHeader(e) => e.fmt(f), + Self::Key(e) => e.fmt(f), + Self::Value(e) => e.fmt(f), + Self::Newline(e) => e.fmt(f), + Self::ValueNotDone(e) => e.fmt(f), + Self::ValueDone(e) => e.fmt(f), + Self::Whitespace(e) => e.fmt(f), + } + } +} + /// A parsed section containing the header and the section events. #[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Default)] pub struct ParsedSection<'a> { @@ -59,6 +77,15 @@ pub struct ParsedSection<'a> { /// The syntactic events found in this section. pub events: Vec>, } +impl Display for ParsedSection<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.section_header)?; + for event in &self.events { + event.fmt(f)?; + } + Ok(()) + } +} /// A parsed section header, containing a name and optionally a subsection name. #[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Default)] @@ -75,6 +102,23 @@ pub struct ParsedSectionHeader<'a> { pub subsection_name: Option>, } +impl Display for ParsedSectionHeader<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "[{}", self.name)?; + + if let Some(v) = &self.separator { + v.fmt(f)?; + let subsection_name = self.subsection_name.as_ref().unwrap(); + match v.borrow() { + "." => subsection_name.fmt(f)?, + _ => write!(f, "\"{}\"", subsection_name)?, + } + } + + write!(f, "]") + } +} + /// A parsed comment event containing the comment marker and comment. #[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Default)] pub struct ParsedComment<'a> { @@ -84,6 +128,13 @@ pub struct ParsedComment<'a> { pub comment: Cow<'a, str>, } +impl Display for ParsedComment<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.comment_tag.fmt(f)?; + self.comment.fmt(f) + } +} + /// The various parsing failure reasons. #[derive(PartialEq, Debug)] pub enum ParserError<'a> { @@ -275,7 +326,7 @@ impl<'a> From>> for ParserError<'a> { /// [`FromStr`]: std::str::FromStr #[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Default)] pub struct Parser<'a> { - init_comments: Vec>, + frontmatter: Vec>, sections: Vec>, } @@ -296,20 +347,21 @@ impl<'a> Parser<'a> { parse_from_str(s) } - /// Returns the leading comments (any comments before a section) from the - /// parser. Consider [`Parser::take_leading_comments`] if you need an owned - /// copy only once. If that function was called, then this will always - /// return an empty slice. - pub fn leading_comments(&self) -> &[ParsedComment<'a>] { - &self.init_comments + /// Returns the leading events (any comments, whitespace, or newlines before + /// a section) from the parser. Consider [`Parser::take_frontmatter`] if + /// you need an owned copy only once. If that function was called, then this + /// will always return an empty slice. + pub fn frontmatter(&self) -> &[Event<'a>] { + &self.frontmatter } - /// Takes the leading comments (any comments before a section) from the - /// parser. Subsequent calls will return an empty vec. Consider - /// [`Parser::leading_comments`] if you only need a reference to the comments. - pub fn take_leading_comments(&mut self) -> Vec> { + /// Takes the leading events (any comments, whitespace, or newlines before + /// a section) from the parser. Subsequent calls will return an empty vec. + /// Consider [`Parser::frontmatter`] if you only need a reference to the + /// frontmatter + pub fn take_frontmatter(&mut self) -> Vec> { let mut to_return = vec![]; - std::mem::swap(&mut self.init_comments, &mut to_return); + std::mem::swap(&mut self.frontmatter, &mut to_return); to_return } @@ -345,10 +397,7 @@ impl<'a> Parser<'a> { .chain(section.events) }) .flatten(); - self.init_comments - .into_iter() - .map(Event::Comment) - .chain(section_iter) + self.frontmatter.into_iter().chain(section_iter) } } @@ -363,7 +412,13 @@ impl<'a> Parser<'a> { /// This generally is due to either invalid names or if there's extraneous /// data succeeding valid `git-config` data. pub fn parse_from_str(input: &str) -> Result, ParserError> { - let (i, comments) = many0(comment)(input)?; + let (i, frontmatter) = many0(alt(( + map(comment, |comment| Event::Comment(comment)), + map(take_spaces, |whitespace| { + Event::Whitespace(whitespace.into()) + }), + map(take_newline, |newline| Event::Newline(newline.into())), + )))(input)?; let (i, sections) = many0(section)(i)?; if !i.is_empty() { @@ -371,7 +426,7 @@ pub fn parse_from_str(input: &str) -> Result, ParserError> { } Ok(Parser { - init_comments: comments, + frontmatter, sections, }) } diff --git a/tests/parser_integration_tests.rs b/tests/parser_integration_tests.rs index 80bc425..adea606 100644 --- a/tests/parser_integration_tests.rs +++ b/tests/parser_integration_tests.rs @@ -170,3 +170,25 @@ fn personal_config() { fn parse_empty() { assert_eq!(parse_from_str("").unwrap().into_vec(), vec![]); } + +#[test] +fn parse_whitespace() { + assert_eq!( + parse_from_str("\n \n \n").unwrap().into_vec(), + vec![ + newline(), + whitespace(" "), + newline(), + whitespace(" "), + newline(), + ] + ) +} + +#[test] +fn newline_events_are_merged() { + assert_eq!( + parse_from_str("\n\n\n\n\n").unwrap().into_vec(), + vec![Event::Newline("\n\n\n\n\n".into())] + ); +}