gitconfig writing to string

This commit is contained in:
Edward Shen 2021-02-21 21:16:08 -05:00
parent 7f7a7e073d
commit f10afb7894
Signed by: edward
GPG key ID: 19182661E818369F
3 changed files with 254 additions and 41 deletions

View file

@ -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 serde::Serialize;
use std::borrow::Cow;
use std::collections::{HashMap, VecDeque}; use std::collections::{HashMap, VecDeque};
use std::{borrow::Cow, fmt::Display};
#[derive(Debug, PartialEq, Eq)] #[derive(Debug, PartialEq, Eq)]
pub enum GitConfigError<'a> { pub enum GitConfigError<'a> {
@ -23,7 +23,7 @@ pub struct GitConfig<'a> {
/// SectionId to section mapping. The value of this HashMap contains actual /// SectionId to section mapping. The value of this HashMap contains actual
/// events /// events
sections: HashMap<SectionId, Vec<Event<'a>>>, sections: HashMap<SectionId, Vec<Event<'a>>>,
section_header_separators: HashMap<SectionId, Option<Cow<'a, str>>>, section_headers: HashMap<SectionId, ParsedSectionHeader<'a>>,
section_id_counter: usize, section_id_counter: usize,
section_order: VecDeque<SectionId>, section_order: VecDeque<SectionId>,
} }
@ -60,7 +60,7 @@ impl<'a> GitConfig<'a> {
front_matter_events: vec![], front_matter_events: vec![],
sections: HashMap::new(), sections: HashMap::new(),
section_lookup_tree: HashMap::new(), section_lookup_tree: HashMap::new(),
section_header_separators: HashMap::new(), section_headers: HashMap::new(),
section_id_counter: 0, section_id_counter: 0,
section_order: VecDeque::new(), section_order: VecDeque::new(),
}; };
@ -80,16 +80,16 @@ impl<'a> GitConfig<'a> {
); );
// Initialize new section // 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 // We need to store the new, current id counter, so don't
// use new_section_id here and use the already incremented // use new_section_id here and use the already incremented
// section id value. // section id value.
new_self new_self
.section_header_separators .section_headers
.insert(SectionId(new_self.section_id_counter), header.separator); .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::Key(_)
| e @ Event::Value(_) | e @ Event::Value(_)
@ -513,6 +513,62 @@ impl<'a> GitConfig<'a> {
*value = new_value.into(); *value = new_value.into();
Ok(()) 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<Cow<'a, str>>,
) -> 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)] #[cfg(test)]
@ -522,7 +578,7 @@ mod from_parser {
#[test] #[test]
fn parse_empty() { fn parse_empty() {
let config = GitConfig::from_str("").unwrap(); 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_eq!(config.section_id_counter, 0);
assert!(config.section_lookup_tree.is_empty()); assert!(config.section_lookup_tree.is_empty());
assert!(config.sections.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 mut config = GitConfig::from_str("[core]\na=b\nc=d").unwrap();
let expected_separators = { let expected_separators = {
let mut map = HashMap::new(); let mut map = HashMap::new();
map.insert(SectionId(0), None); map.insert(
SectionId(0),
ParsedSectionHeader {
name: "core".into(),
separator: None,
subsection_name: None,
},
);
map map
}; };
assert_eq!(config.section_header_separators, expected_separators); assert_eq!(config.section_headers, expected_separators);
assert_eq!(config.section_id_counter, 1); assert_eq!(config.section_id_counter, 1);
let expected_lookup_tree = { let expected_lookup_tree = {
let mut tree = HashMap::new(); 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 mut config = GitConfig::from_str("[core.subsec]\na=b\nc=d").unwrap();
let expected_separators = { let expected_separators = {
let mut map = HashMap::new(); 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 map
}; };
assert_eq!(config.section_header_separators, expected_separators); assert_eq!(config.section_headers, expected_separators);
assert_eq!(config.section_id_counter, 1); assert_eq!(config.section_id_counter, 1);
let expected_lookup_tree = { let expected_lookup_tree = {
let mut tree = HashMap::new(); 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 mut config = GitConfig::from_str("[core]\na=b\nc=d\n[other]e=f").unwrap();
let expected_separators = { let expected_separators = {
let mut map = HashMap::new(); let mut map = HashMap::new();
map.insert(SectionId(0), None); map.insert(
map.insert(SectionId(1), None); 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 map
}; };
assert_eq!(config.section_header_separators, expected_separators); assert_eq!(config.section_headers, expected_separators);
assert_eq!(config.section_id_counter, 2); assert_eq!(config.section_id_counter, 2);
let expected_lookup_tree = { let expected_lookup_tree = {
let mut tree = HashMap::new(); 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 mut config = GitConfig::from_str("[core]\na=b\nc=d\n[core]e=f").unwrap();
let expected_separators = { let expected_separators = {
let mut map = HashMap::new(); let mut map = HashMap::new();
map.insert(SectionId(0), None); map.insert(
map.insert(SectionId(1), None); 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 map
}; };
assert_eq!(config.section_header_separators, expected_separators); assert_eq!(config.section_headers, expected_separators);
assert_eq!(config.section_id_counter, 2); assert_eq!(config.section_id_counter, 2);
let expected_lookup_tree = { let expected_lookup_tree = {
let mut tree = HashMap::new(); 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);
}
}

View file

@ -17,7 +17,9 @@ use nom::error::{Error as NomError, ErrorKind};
use nom::sequence::delimited; use nom::sequence::delimited;
use nom::IResult; use nom::IResult;
use nom::{branch::alt, multi::many0}; 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. /// Syntactic events that occurs in the config.
#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)] #[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
@ -38,7 +40,8 @@ pub enum Event<'a> {
Value(Cow<'a, str>), Value(Cow<'a, str>),
/// Represents any token used to signify a new line character. On Unix /// Represents any token used to signify a new line character. On Unix
/// platforms, this is typically just `\n`, but can be any valid newline /// 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>), Newline(Cow<'a, str>),
/// Any value that isn't completed. This occurs when the value is continued /// 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 /// onto the next line. A Newline event is guaranteed after, followed by
@ -51,6 +54,21 @@ pub enum Event<'a> {
Whitespace(Cow<'a, str>), 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. /// A parsed section containing the header and the section events.
#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Default)] #[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Default)]
pub struct ParsedSection<'a> { pub struct ParsedSection<'a> {
@ -59,6 +77,15 @@ pub struct ParsedSection<'a> {
/// The syntactic events found in this section. /// The syntactic events found in this section.
pub events: Vec<Event<'a>>, pub events: Vec<Event<'a>>,
} }
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. /// A parsed section header, containing a name and optionally a subsection name.
#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Default)] #[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Default)]
@ -75,6 +102,23 @@ pub struct ParsedSectionHeader<'a> {
pub subsection_name: Option<Cow<'a, str>>, pub subsection_name: Option<Cow<'a, str>>,
} }
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. /// A parsed comment event containing the comment marker and comment.
#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Default)] #[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Default)]
pub struct ParsedComment<'a> { pub struct ParsedComment<'a> {
@ -84,6 +128,13 @@ pub struct ParsedComment<'a> {
pub comment: Cow<'a, str>, 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. /// The various parsing failure reasons.
#[derive(PartialEq, Debug)] #[derive(PartialEq, Debug)]
pub enum ParserError<'a> { pub enum ParserError<'a> {
@ -275,7 +326,7 @@ impl<'a> From<nom::Err<NomError<&'a str>>> for ParserError<'a> {
/// [`FromStr`]: std::str::FromStr /// [`FromStr`]: std::str::FromStr
#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Default)] #[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Default)]
pub struct Parser<'a> { pub struct Parser<'a> {
init_comments: Vec<ParsedComment<'a>>, frontmatter: Vec<Event<'a>>,
sections: Vec<ParsedSection<'a>>, sections: Vec<ParsedSection<'a>>,
} }
@ -296,20 +347,21 @@ impl<'a> Parser<'a> {
parse_from_str(s) parse_from_str(s)
} }
/// Returns the leading comments (any comments before a section) from the /// Returns the leading events (any comments, whitespace, or newlines before
/// parser. Consider [`Parser::take_leading_comments`] if you need an owned /// a section) from the parser. Consider [`Parser::take_frontmatter`] if
/// copy only once. If that function was called, then this will always /// you need an owned copy only once. If that function was called, then this
/// return an empty slice. /// will always return an empty slice.
pub fn leading_comments(&self) -> &[ParsedComment<'a>] { pub fn frontmatter(&self) -> &[Event<'a>] {
&self.init_comments &self.frontmatter
} }
/// Takes the leading comments (any comments before a section) from the /// Takes the leading events (any comments, whitespace, or newlines before
/// parser. Subsequent calls will return an empty vec. Consider /// a section) from the parser. Subsequent calls will return an empty vec.
/// [`Parser::leading_comments`] if you only need a reference to the comments. /// Consider [`Parser::frontmatter`] if you only need a reference to the
pub fn take_leading_comments(&mut self) -> Vec<ParsedComment<'a>> { /// frontmatter
pub fn take_frontmatter(&mut self) -> Vec<Event<'a>> {
let mut to_return = 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 to_return
} }
@ -345,10 +397,7 @@ impl<'a> Parser<'a> {
.chain(section.events) .chain(section.events)
}) })
.flatten(); .flatten();
self.init_comments self.frontmatter.into_iter().chain(section_iter)
.into_iter()
.map(Event::Comment)
.chain(section_iter)
} }
} }
@ -363,7 +412,13 @@ impl<'a> Parser<'a> {
/// This generally is due to either invalid names or if there's extraneous /// This generally is due to either invalid names or if there's extraneous
/// data succeeding valid `git-config` data. /// data succeeding valid `git-config` data.
pub fn parse_from_str(input: &str) -> Result<Parser<'_>, ParserError> { pub fn parse_from_str(input: &str) -> Result<Parser<'_>, 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)?; let (i, sections) = many0(section)(i)?;
if !i.is_empty() { if !i.is_empty() {
@ -371,7 +426,7 @@ pub fn parse_from_str(input: &str) -> Result<Parser<'_>, ParserError> {
} }
Ok(Parser { Ok(Parser {
init_comments: comments, frontmatter,
sections, sections,
}) })
} }

View file

@ -170,3 +170,25 @@ fn personal_config() {
fn parse_empty() { fn parse_empty() {
assert_eq!(parse_from_str("").unwrap().into_vec(), vec![]); 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())]
);
}