gitconfig writing to string
This commit is contained in:
parent
7f7a7e073d
commit
f10afb7894
3 changed files with 254 additions and 41 deletions
178
src/config.rs
178
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<SectionId, Vec<Event<'a>>>,
|
||||
section_header_separators: HashMap<SectionId, Option<Cow<'a, str>>>,
|
||||
section_headers: HashMap<SectionId, ParsedSectionHeader<'a>>,
|
||||
section_id_counter: usize,
|
||||
section_order: VecDeque<SectionId>,
|
||||
}
|
||||
|
@ -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<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)]
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<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.
|
||||
#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Default)]
|
||||
|
@ -75,6 +102,23 @@ pub struct ParsedSectionHeader<'a> {
|
|||
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.
|
||||
#[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<nom::Err<NomError<&'a str>>> for ParserError<'a> {
|
|||
/// [`FromStr`]: std::str::FromStr
|
||||
#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Default)]
|
||||
pub struct Parser<'a> {
|
||||
init_comments: Vec<ParsedComment<'a>>,
|
||||
frontmatter: Vec<Event<'a>>,
|
||||
sections: Vec<ParsedSection<'a>>,
|
||||
}
|
||||
|
||||
|
@ -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<ParsedComment<'a>> {
|
||||
/// 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<Event<'a>> {
|
||||
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<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)?;
|
||||
|
||||
if !i.is_empty() {
|
||||
|
@ -371,7 +426,7 @@ pub fn parse_from_str(input: &str) -> Result<Parser<'_>, ParserError> {
|
|||
}
|
||||
|
||||
Ok(Parser {
|
||||
init_comments: comments,
|
||||
frontmatter,
|
||||
sections,
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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())]
|
||||
);
|
||||
}
|
||||
|
|
Reference in a new issue