Compare commits

..

94 commits

Author SHA1 Message Date
88fec2a7e4
rename_section 2021-03-09 11:23:40 -05:00
b73d97f24a
return key iterator 2021-03-09 11:08:57 -05:00
f97abaf30d
add push_section 2021-03-09 11:04:55 -05:00
801d808ab4
more work on sections 2021-03-08 20:08:17 -05:00
f136235205
remove offset newtype 2021-03-08 17:15:56 -05:00
1e4f7189a5
remove section for owned section type 2021-03-08 17:12:59 -05:00
65506c733f
more functionality to mutablesection 2021-03-08 16:29:47 -05:00
3c83e46a30
optimize section pushing 2021-03-08 15:53:16 -05:00
1cde32efd1
section API 2021-03-07 11:59:09 -05:00
32fe1612f5
section stuct 2021-03-06 21:25:21 -05:00
ca55bff79d
fix example 2021-03-06 11:46:04 -05:00
bd29f67142
update readme 2021-03-06 01:51:05 -05:00
0638e00677
fix macro comment gen 2021-03-06 01:28:28 -05:00
9715790574
implement case insensitivity for names 2021-03-06 01:23:23 -05:00
6167914277
test MutableMultiValue 2021-03-05 23:34:07 -05:00
05eba761ee
more tests, fix mutablevalue 2021-03-05 18:58:19 -05:00
ce2529fa9d
fix lints 2021-03-05 16:34:29 -05:00
a345a8d93e
enable requiring docs 2021-03-05 16:28:04 -05:00
7c29e44f20
clippy fix 2021-03-03 20:43:00 -05:00
1354d931b2
docs 2021-03-03 20:38:46 -05:00
6ce2c1d89d
add into bytes for gitconfig 2021-03-03 20:11:58 -05:00
ebfd15b6d3
docs 2021-03-03 18:44:28 -05:00
7cbdd46fde
mutableevent interface 2021-03-03 18:16:24 -05:00
b78ef63bdf
remove serde code for now 2021-03-03 18:14:57 -05:00
23ae291361
disable serde 2021-03-03 18:13:35 -05:00
cbb970c293
multablemultivalue 2021-03-02 14:34:18 -05:00
559afb01fa
better test formatting 2021-03-02 11:28:16 -05:00
60fe0deb91
Add get_multi_value 2021-03-02 11:26:32 -05:00
925a3b4afa
check all sections for lookup before failing 2021-03-01 23:59:38 -05:00
0905642feb
misc improvements 2021-03-01 18:33:26 -05:00
8778d42e78
benchmarks 2021-03-01 18:33:07 -05:00
068deccf96
crate level docs 2021-03-01 18:15:19 -05:00
5574ec3267
integration tests for value extraction 2021-03-01 17:00:47 -05:00
e9598831cc
fix drain 2021-03-01 16:37:34 -05:00
cd2f58c920
use memrchr 2021-03-01 16:01:47 -05:00
c975a2ec14
use drain instead 2021-03-01 15:39:18 -05:00
c9c8e70afb
use mutablevalue for mut entries 2021-02-28 22:47:40 -05:00
493729cc3c
normalize get_raw_value 2021-02-28 21:07:02 -05:00
c01b61cb11
fix get_raw_value, fix returning refs to cows 2021-02-28 20:42:54 -05:00
5720ccd003
cleanup docs 2021-02-27 23:50:58 -05:00
185e1129bc
add tests and docs 2021-02-27 23:46:56 -05:00
42a20c3dad
rename config mod to file 2021-02-27 23:21:33 -05:00
0ce311a1eb
pendantic clippy lints 2021-02-27 23:19:25 -05:00
3ff68bfaf8
remove unnecessarily lifetimes 2021-02-27 22:22:33 -05:00
bfd4172e48
use str in most cases 2021-02-27 22:18:44 -05:00
65744b0e13
fully comment values 2021-02-27 19:55:19 -05:00
37cead20f3
more normalize docs 2021-02-27 19:14:49 -05:00
267c53f15d
collaspe if block 2021-02-27 19:07:50 -05:00
60f95a0358
better doc 2021-02-27 19:01:01 -05:00
99f0400118
implement unquoting in normalize 2021-02-27 18:48:05 -05:00
300fb6bbfb
add normalize 2021-02-27 15:40:11 -05:00
48d41b81e9
dedup multivar docs 2021-02-27 11:13:20 -05:00
bac41a802a
add todo 2021-02-27 00:10:43 -05:00
16496d91a1
Implement get_value for GitConfig 2021-02-26 23:33:38 -05:00
3e97f07b28
Use traits instead of from_str 2021-02-26 21:44:58 -05:00
a50544b43a
Use traits instead of shadowing from_str 2021-02-26 21:32:37 -05:00
781040a88b
remove falsevariant 2021-02-26 21:24:55 -05:00
83c4757e36
more tests 2021-02-26 20:50:52 -05:00
2fadd81287
remove unreachable variants 2021-02-26 20:36:21 -05:00
17ba292934
use mut vec reference 2021-02-26 19:30:04 -05:00
6b8386d449
Don't use mutex 2021-02-26 18:54:35 -05:00
531b28ed2b
documented parsererror 2021-02-26 18:35:49 -05:00
ba312b9244
clippy fixes 2021-02-26 18:14:40 -05:00
df69cf8dba
don't use stack for error handling 2021-02-26 18:10:24 -05:00
6b9fb8f8e5
very rough error handling 2021-02-25 11:11:26 -05:00
23e2a37785
add error trait impl for ParserError 2021-02-24 23:23:23 -05:00
a10567e770
Basic error reporting 2021-02-24 23:19:45 -05:00
42a48efe9d
move fully_sumed to test_util 2021-02-24 18:00:15 -05:00
19e18df973
Don't immediately drop fuzzer values 2021-02-24 17:55:58 -05:00
38dc0f0b8f
Add more fields to cargo.toml 2021-02-24 16:44:04 -05:00
0bd39a308c
Add basic fuzzer 2021-02-24 16:30:14 -05:00
f99da79ad9
add from_bytes variants for parser 2021-02-24 16:30:04 -05:00
df6937ade1
exclude fuzz folder from cargo 2021-02-24 16:29:40 -05:00
19cb3117eb
add tests for boolean 2021-02-24 15:47:35 -05:00
48d7e1b65b
Use lto and single codegen unit for release 2021-02-24 13:09:52 -05:00
3094dfd2c0
select nom features 2021-02-24 12:30:49 -05:00
75a99679a2
make serde optional, clippy lints 2021-02-24 12:11:18 -05:00
3f8fdd74dc
Add ColorValue tests 2021-02-23 21:02:17 -05:00
a53a056cea
Add tests for ColorAttribute 2021-02-23 20:45:13 -05:00
6c2385d394
Fix docs 2021-02-23 20:22:15 -05:00
ea0f76d528
Use BStr instead 2021-02-23 19:47:24 -05:00
f82d32953e
Add key-value delimination event 2021-02-23 11:30:48 -05:00
f10afb7894
gitconfig writing to string 2021-02-21 21:16:08 -05:00
7f7a7e073d
document multivar behavior better 2021-02-21 19:08:27 -05:00
6a99b1caa0
Use Cow instead of strs 2021-02-21 14:43:41 -05:00
eaa0a1766b
Implement get_mut for gitconfig 2021-02-20 20:41:34 -05:00
fcda1d4666
remove meme comment 2021-02-20 14:12:08 -05:00
3bd46913d3
test get_raw_values 2021-02-20 14:10:23 -05:00
594ee8ac0c
finish raw value queries for gitconfig 2021-02-20 13:49:01 -05:00
2fb1fa3ff4
Handle empty git-config file for parser 2021-02-20 12:01:38 -05:00
93e6e2eed3
fully document parser 2021-02-20 02:28:50 -05:00
1c770a9560
completely refactor config 2021-02-20 00:33:17 -05:00
19fc0ebaaf
Booleans now retain original value 2021-02-19 19:41:58 -05:00
c244975a0a
parser is now perfect 2021-02-19 19:18:02 -05:00
23 changed files with 5929 additions and 1649 deletions

1
.gitignore vendored
View file

@ -1,2 +1,3 @@
/target
Cargo.lock
tarpaulin-report.html

13
.vscode/settings.json vendored Normal file
View file

@ -0,0 +1,13 @@
{
"cSpell.words": [
"Multivar",
"autocrlf",
"bstr",
"combinator",
"gitea",
"gpgsign",
"implicits",
"multivars",
"subname"
]
}

View file

@ -1,14 +1,31 @@
[package]
name = "serde-git-config"
name = "git-config"
version = "0.1.0"
repository = "https://github.com/Byron/gitoxide"
description = "A git-config file parser and editor from the gitoxide project"
license = "MIT OR Apache-2.0"
authors = ["Edward Shen <code@eddie.sh>"]
edition = "2018"
keywords = ["git-config", "git", "config", "gitoxide"]
categories = ["config", "parser-implementations"]
exclude = ["fuzz/**/*", ".vscode/**/*", "benches/**/*"]
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[features]
# serde = ["serde_crate"]
[dependencies]
serde = "1.0"
nom = "6"
memchr = "2"
nom = { version = "6", default_features = false, features = ["std"] }
serde_crate = { version = "1", package = "serde", optional = true }
[dev-dependencies]
serde_derive = "1.0"
serde_derive = "1.0"
criterion = "0.3"
[profile.release]
lto = true
codegen-units = 1
[[bench]]
name = "large_config_file"
harness = false

1
LICENSE-APACHE Symbolic link
View file

@ -0,0 +1 @@
../LICENSE-APACHE

1
LICENSE-MIT Symbolic link
View file

@ -0,0 +1 @@
../LICENSE-MIT

75
README.md Normal file
View file

@ -0,0 +1,75 @@
# git-config
**git-config is a library for interacting with `git-config` files.**
This crate intents to be a performant Rust implementation for reading and
writing `git-config` files. It exposes tiers of abstractions, from simple
config value wrappers to a high level reader and writer.
The highlight of this crate is the zero-copy parser. We employ techniques to
avoid copying where necessary, and reads that do not need normalization are
guaranteed to be zero-copy. Higher level abstractions maintain this guarantee,
and utilizes acceleration structures for increased performance.
Currently, this is _not_ a binary. While we do intent to have a drop-in
replacement for the `git config` sub-command, we're currently missing
system-level abstractions to do so.
## Examples
Reading and writing to a config:
```rust
use git_config::file::GitConfig;
use git_config::values::Boolean;
use std::fs::read_to_string;
let input = r#"
[core]
some-bool = true
[other "internal"]
hello = world
"#;
let mut config = GitConfig::from(input)?;
let boolean = config.get_config::<Boolean>("core", None, "some-bool");
config.set_value("other", Some("internal"), "hello", "clippy!");
```
## Contributing
Contributions are always welcome!
### Code quality
This repository enables pedantic, cargo, and nursery `clippy` lints. Make sure
to run `cargo clean && cargo clippy` (the clean stage is very important!) to
ensure your code is linted.
### Testing
Since this is a performance oriented crate, in addition to well tested code via
`cargo test`, we also perform benchmarks to measure notable gains or losses in
performance. We use [`criterion`] so benches can be run via `cargo bench` after
installing it via `cargo install cargo-criterion`.
Changes to `parser.rs` may include a request to fuzz to ensure that it cannot
panic on inputs. This can be done by executing `cargo fuzz parser` after
installing the `fuzz` sub-command via `cargo install cargo-fuzz`.
#### License
<sup>
Licensed under either of <a href="LICENSE-APACHE">Apache License, Version
2.0</a> or <a href="LICENSE-MIT">MIT license</a> at your option.
</sup>
<br>
<sub>
Unless you explicitly state otherwise, any contribution intentionally submitted
for inclusion in git-config by you, as defined in the Apache-2.0 license, shall
be dual licensed as above, without any additional terms or conditions.
</sub>
[`criterion`]: https://github.com/bheisler/criterion.rs

View file

@ -0,0 +1,293 @@
use std::convert::TryFrom;
use criterion::{black_box, criterion_group, criterion_main, Criterion};
use git_config::{file::GitConfig, parser::Parser};
fn git_config(c: &mut Criterion) {
c.bench_function("GitConfig large config file", |b| {
b.iter(|| GitConfig::try_from(black_box(CONFIG_FILE)).unwrap())
});
}
fn parser(c: &mut Criterion) {
c.bench_function("Parser large config file", |b| {
b.iter(|| Parser::try_from(black_box(CONFIG_FILE)).unwrap())
});
}
criterion_group!(benches, git_config, parser);
criterion_main!(benches);
// Found from https://gist.github.com/pksunkara/988716
const CONFIG_FILE: &str = r#"[user]
name = Pavan Kumar Sunkara
email = pavan.sss1991@gmail.com
username = pksunkara
[core]
editor = vim
whitespace = fix,-indent-with-non-tab,trailing-space,cr-at-eol
pager = delta
[sendemail]
smtpencryption = tls
smtpserver = smtp.gmail.com
smtpuser = pavan.sss1991@gmail.com
smtppass = password
smtpserverport = 587
[web]
browser = google-chrome
[instaweb]
httpd = apache2 -f
[rerere]
enabled = 1
autoupdate = 1
[push]
default = matching
[color]
ui = auto
[color "branch"]
current = yellow bold
local = green bold
remote = cyan bold
[color "diff"]
meta = yellow bold
frag = magenta bold
old = red bold
new = green bold
whitespace = red reverse
[color "status"]
added = green bold
changed = yellow bold
untracked = red bold
[diff]
tool = vimdiff
[difftool]
prompt = false
[delta]
features = line-numbers decorations
line-numbers = true
[delta "decorations"]
minus-style = red bold normal
plus-style = green bold normal
minus-emph-style = white bold red
minus-non-emph-style = red bold normal
plus-emph-style = white bold green
plus-non-emph-style = green bold normal
file-style = yellow bold none
file-decoration-style = yellow box
hunk-header-style = magenta bold
hunk-header-decoration-style = magenta box
minus-empty-line-marker-style = normal normal
plus-empty-line-marker-style = normal normal
line-numbers-right-format = "{np:^4}│ "
[github]
user = pksunkara
token = token
[gitflow "prefix"]
versiontag = v
[sequence]
editor = interactive-rebase-tool
[alias]
a = add --all
ai = add -i
#############
ap = apply
as = apply --stat
ac = apply --check
#############
ama = am --abort
amr = am --resolved
ams = am --skip
#############
b = branch
ba = branch -a
bd = branch -d
bdd = branch -D
br = branch -r
bc = rev-parse --abbrev-ref HEAD
bu = !git rev-parse --abbrev-ref --symbolic-full-name "@{u}"
bs = !git-branch-status
#############
c = commit
ca = commit -a
cm = commit -m
cam = commit -am
cem = commit --allow-empty -m
cd = commit --amend
cad = commit -a --amend
ced = commit --allow-empty --amend
#############
cl = clone
cld = clone --depth 1
clg = !sh -c 'git clone git://github.com/$1 $(basename $1)' -
clgp = !sh -c 'git clone git@github.com:$1 $(basename $1)' -
clgu = !sh -c 'git clone git@github.com:$(git config --get user.username)/$1 $1' -
#############
cp = cherry-pick
cpa = cherry-pick --abort
cpc = cherry-pick --continue
#############
d = diff
dp = diff --patience
dc = diff --cached
dk = diff --check
dck = diff --cached --check
dt = difftool
dct = difftool --cached
#############
f = fetch
fo = fetch origin
fu = fetch upstream
#############
fp = format-patch
#############
fk = fsck
#############
g = grep -p
#############
l = log --oneline
lg = log --oneline --graph --decorate
#############
ls = ls-files
lsf = !git ls-files | grep -i
#############
m = merge
ma = merge --abort
mc = merge --continue
ms = merge --skip
#############
o = checkout
om = checkout master
ob = checkout -b
opr = !sh -c 'git fo pull/$1/head:pr-$1 && git o pr-$1'
#############
pr = prune -v
#############
ps = push
psf = push -f
psu = push -u
pst = push --tags
#############
pso = push origin
psao = push --all origin
psfo = push -f origin
psuo = push -u origin
#############
psom = push origin master
psaom = push --all origin master
psfom = push -f origin master
psuom = push -u origin master
psoc = !git push origin $(git bc)
psaoc = !git push --all origin $(git bc)
psfoc = !git push -f origin $(git bc)
psuoc = !git push -u origin $(git bc)
psdc = !git push origin :$(git bc)
#############
pl = pull
pb = pull --rebase
#############
plo = pull origin
pbo = pull --rebase origin
plom = pull origin master
ploc = !git pull origin $(git bc)
pbom = pull --rebase origin master
pboc = !git pull --rebase origin $(git bc)
#############
plu = pull upstream
plum = pull upstream master
pluc = !git pull upstream $(git bc)
pbum = pull --rebase upstream master
pbuc = !git pull --rebase upstream $(git bc)
#############
rb = rebase
rba = rebase --abort
rbc = rebase --continue
rbi = rebase --interactive
rbs = rebase --skip
#############
re = reset
rh = reset HEAD
reh = reset --hard
rem = reset --mixed
res = reset --soft
rehh = reset --hard HEAD
remh = reset --mixed HEAD
resh = reset --soft HEAD
rehom = reset --hard origin/master
#############
r = remote
ra = remote add
rr = remote rm
rv = remote -v
rn = remote rename
rp = remote prune
rs = remote show
rao = remote add origin
rau = remote add upstream
rro = remote remove origin
rru = remote remove upstream
rso = remote show origin
rsu = remote show upstream
rpo = remote prune origin
rpu = remote prune upstream
#############
rmf = rm -f
rmrf = rm -r -f
#############
s = status
sb = status -s -b
#############
sa = stash apply
sc = stash clear
sd = stash drop
sl = stash list
sp = stash pop
ss = stash save
ssk = stash save -k
sw = stash show
st = !git stash list | wc -l 2>/dev/null | grep -oEi '[0-9][0-9]*'
#############
t = tag
td = tag -d
#############
w = show
wp = show -p
wr = show -p --no-color
#############
svnr = svn rebase
svnd = svn dcommit
svnl = svn log --oneline --show-commit
#############
subadd = !sh -c 'git submodule add git://github.com/$1 $2/$(basename $1)' -
subrm = !sh -c 'git submodule deinit -f -- $1 && rm -rf .git/modules/$1 && git rm -f $1' -
subup = submodule update --init --recursive
subpull = !git submodule foreach git pull --tags origin master
#############
assume = update-index --assume-unchanged
unassume = update-index --no-assume-unchanged
assumed = !git ls -v | grep ^h | cut -c 3-
unassumeall = !git assumed | xargs git unassume
assumeall = !git status -s | awk {'print $2'} | xargs git assume
#############
bump = !sh -c 'git commit -am \"Version bump v$1\" && git psuoc && git release $1' -
release = !sh -c 'git tag v$1 && git pst' -
unrelease = !sh -c 'git tag -d v$1 && git pso :v$1' -
merged = !sh -c 'git o master && git plom && git bd $1 && git rpo' -
aliases = !git config -l | grep alias | cut -c 7-
snap = !git stash save 'snapshot: $(date)' && git stash apply 'stash@{0}'
bare = !sh -c 'git symbolic-ref HEAD refs/heads/$1 && git rm --cached -r . && git clean -xfd' -
whois = !sh -c 'git log -i -1 --author=\"$1\" --pretty=\"format:%an <%ae>\"' -
serve = daemon --reuseaddr --verbose --base-path=. --export-all ./.git
#############
behind = !git rev-list --left-only --count $(git bu)...HEAD
ahead = !git rev-list --right-only --count $(git bu)...HEAD
#############
ours = "!f() { git checkout --ours $@ && git add $@; }; f"
theirs = "!f() { git checkout --theirs $@ && git add $@; }; f"
subrepo = !sh -c 'git filter-branch --prune-empty --subdirectory-filter $1 master' -
human = name-rev --name-only --refs=refs/heads/*
[filter "lfs"]
clean = git-lfs clean -- %f
smudge = git-lfs smudge -- %f
process = git-lfs filter-process
required = true
"#;

4
fuzz/.gitignore vendored Normal file
View file

@ -0,0 +1,4 @@
target
corpus
artifacts

26
fuzz/Cargo.toml Normal file
View file

@ -0,0 +1,26 @@
[package]
name = "git-config-fuzz"
version = "0.0.0"
authors = ["Automatically generated"]
publish = false
edition = "2018"
[package.metadata]
cargo-fuzz = true
[dependencies]
libfuzzer-sys = "0.3"
[dependencies.git-config]
path = ".."
# Prevent this from interfering with workspaces
[workspace]
members = ["."]
[[bin]]
name = "parser"
path = "fuzz_targets/parser.rs"
test = false
doc = false

View file

@ -0,0 +1,9 @@
#![no_main]
use git_config::parser::Parser;
use libfuzzer_sys::fuzz_target;
fuzz_target!(|data: &[u8]| {
// Don't name this _; Rust may optimize it out.
let _a = Parser::from_bytes(data);
});

View file

@ -1,221 +0,0 @@
use crate::parser::{parse_from_str, Event, ParsedSectionHeader, Parser};
use crate::values::Value;
use serde::{Deserialize, Serialize};
use std::{collections::HashMap, convert::TryFrom, io::Read};
type SectionConfig<'a> = HashMap<&'a str, Value<'a>>;
/// This struct provides a high level wrapper to access `git-config` file. This
/// struct exists primarily for reading a config rather than modifying it, as
/// it discards comments and unnecessary whitespace.
#[derive(Clone, Eq, PartialEq, Debug, Default)]
pub struct GitConfig<'a>(HashMap<&'a str, HashMap<&'a str, SectionConfig<'a>>>);
const EMPTY_MARKER: &str = "@"; // Guaranteed to not be a {sub,}section or name.
impl<'a> GitConfig<'a> {
/// Attempts to construct a instance given a [`Parser`] instance.
///
/// This is _not_ a zero-copy operation. Due to how partial values may be
/// provided, we necessarily need to copy and store these values until we
/// are done.
pub fn try_from_parser_with_options(
parser: Parser<'a>,
options: ConfigOptions,
) -> Result<Self, ()> {
Self::try_from_event_iter_with_options(parser.into_iter(), options)
}
pub fn try_from_event_iter_with_options(
iter: impl Iterator<Item = Event<'a>>,
options: ConfigOptions,
) -> Result<Self, ()> {
let mut sections: HashMap<&'a str, HashMap<&'a str, SectionConfig<'a>>> = HashMap::new();
let mut current_section_name = EMPTY_MARKER;
let mut current_subsection_name = EMPTY_MARKER;
let mut ignore_until_next_section = false;
let mut current_key = EMPTY_MARKER;
let mut value_scratch = String::new();
for event in iter {
match event {
Event::Comment(_) => (),
Event::SectionHeader(ParsedSectionHeader {
name,
subsection_name,
}) => {
current_section_name = name;
match (sections.get_mut(name), options.on_duplicate_section) {
(Some(_), OnDuplicateBehavior::Error) => todo!(),
(Some(section), OnDuplicateBehavior::Overwrite) => {
section.clear();
}
(Some(_), OnDuplicateBehavior::KeepExisting) => {
ignore_until_next_section = true;
}
(None, _) => {
sections.insert(name, HashMap::default());
}
}
match subsection_name {
Some(v) => current_subsection_name = v,
None => {
current_subsection_name = EMPTY_MARKER;
continue;
}
};
// subsection parsing
match (
sections
.get_mut(current_section_name)
.unwrap() // Guaranteed to exist at this point
.get_mut(current_subsection_name),
options.on_duplicate_section,
) {
(Some(_), OnDuplicateBehavior::Error) => todo!(),
(Some(section), OnDuplicateBehavior::Overwrite) => section.clear(),
(Some(_), OnDuplicateBehavior::KeepExisting) => {
ignore_until_next_section = true;
}
(None, _) => (),
}
}
_ if ignore_until_next_section => (),
Event::Key(key) => {
current_key = key;
}
Event::Value(v) => {
Self::insert_value(
&mut sections,
current_section_name,
current_subsection_name,
current_key,
v,
options.on_duplicate_name,
)?;
}
Event::Newline(_) => (),
Event::ValueNotDone(v) => value_scratch.push_str(v),
Event::ValueDone(v) => {
let mut completed_value = String::new();
value_scratch.push_str(v);
std::mem::swap(&mut completed_value, &mut value_scratch);
Self::insert_value(
&mut sections,
current_section_name,
current_subsection_name,
current_key,
Value::from_string(completed_value),
options.on_duplicate_name,
)?;
}
}
}
Ok(Self(sections))
}
fn insert_value(
map: &mut HashMap<&'a str, HashMap<&'a str, SectionConfig<'a>>>,
section: &str,
subsection: &str,
key: &'a str,
value: Value<'a>,
on_dup: OnDuplicateBehavior,
) -> Result<(), ()> {
let config = map.get_mut(section).unwrap().get_mut(subsection).unwrap();
if config.contains_key(key) {
match on_dup {
OnDuplicateBehavior::Error => return Err(()),
OnDuplicateBehavior::Overwrite => {
config.insert(key, value);
}
OnDuplicateBehavior::KeepExisting => (),
}
} else {
config.insert(key, value);
}
Ok(())
}
pub fn get_section(&self, section_name: &str) -> Option<&SectionConfig<'_>> {
self.get_subsection(section_name, EMPTY_MARKER)
}
pub fn get_section_value(&self, section_name: &str, key: &str) -> Option<&Value<'_>> {
self.get_section(section_name)
.map(|section| section.get(key))
.flatten()
}
pub fn get_subsection(
&self,
section_name: &str,
subsection_name: &str,
) -> Option<&SectionConfig<'_>> {
self.0
.get(section_name)
.map(|subsections| subsections.get(subsection_name))
.flatten()
}
pub fn get_subsection_value(
&self,
section_name: &str,
subsection_name: &str,
key: &str,
) -> Option<&Value<'_>> {
self.get_subsection(section_name, subsection_name)
.map(|section| section.get(key))
.flatten()
}
}
impl<'a> TryFrom<Parser<'a>> for GitConfig<'a> {
type Error = ();
fn try_from(parser: Parser<'a>) -> Result<Self, Self::Error> {
Self::try_from_parser_with_options(parser, ConfigOptions::default())
}
}
#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Default)]
pub struct ConfigOptions {
on_duplicate_section: OnDuplicateBehavior,
on_duplicate_name: OnDuplicateBehavior,
}
impl ConfigOptions {
pub fn on_duplicate_section(&mut self, behavior: OnDuplicateBehavior) -> &mut Self {
self.on_duplicate_section = behavior;
self
}
pub fn on_duplicate_name(&mut self, behavior: OnDuplicateBehavior) -> &mut Self {
self.on_duplicate_name = behavior;
self
}
}
/// [`GitConfig`]'s valid possible actions when encountering a duplicate section
/// or key name within a section.
#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
pub enum OnDuplicateBehavior {
/// Fail the operation, returning an error instead. This is the strictest
/// behavior, and is the default.
Error,
/// Discard any data we had before on the
Overwrite,
KeepExisting,
}
impl Default for OnDuplicateBehavior {
fn default() -> Self {
Self::Error
}
}

410
src/de.rs
View file

@ -1,410 +0,0 @@
use std::ops::{AddAssign, MulAssign, Neg};
use crate::values::Boolean;
use crate::{
error::{Error, Result},
values::PeekParse,
};
use serde::de::{
self, DeserializeSeed, EnumAccess, IntoDeserializer, MapAccess, SeqAccess, VariantAccess,
Visitor,
};
use serde::Deserialize;
pub struct Deserializer<'de> {
// This string starts with the input data and characters are truncated off
// the beginning as data is parsed.
input: &'de str,
}
impl<'de> Deserializer<'de> {
// By convention, `Deserializer` constructors are named like `from_xyz`.
// That way basic use cases are satisfied by something like
// `serde_json::from_str(...)` while advanced use cases that require a
// deserializer can make one with `serde_json::Deserializer::from_str(...)`.
pub fn from_str(input: &'de str) -> Self {
Deserializer { input }
}
}
// By convention, the public API of a Serde deserializer is one or more
// `from_xyz` methods such as `from_str`, `from_bytes`, or `from_reader`
// depending on what Rust types the deserializer is able to consume as input.
//
// This basic deserializer supports only `from_str`.
pub fn from_str<'a, T>(s: &'a str) -> Result<T>
where
T: Deserialize<'a>,
{
let mut deserializer = Deserializer::from_str(s);
let t = T::deserialize(&mut deserializer)?;
if deserializer.input.is_empty() {
Ok(t)
} else {
todo!()
}
}
impl<'de> Deserializer<'de> {
fn peek(&mut self) -> Result<char> {
self.input.chars().next().ok_or(Error::Eof)
}
fn next(&mut self) -> Result<char> {
let ch = self.peek()?;
self.input = &self.input[ch.len_utf8()..];
Ok(ch)
}
fn parse_bool(&mut self) -> Result<bool> {
let (value, size) = Boolean::peek_parse(self.input)?;
self.input = &self.input[size..];
Ok(value)
}
fn parse_unsigned<T>(&mut self) -> Result<T> {
self.parse_int(true)
}
fn parse_signed<T>(&mut self) -> Result<T> {
self.parse_int(false)
}
fn parse_int<T>(&mut self, positive_only: bool) -> Result<T> {
self.consume_whitespace()?;
match self.next()? {
c @ '0'..='9' => {
let mut significand = (c as u8 - b'0') as u64;
loop {
match self.peek()? {
c @ '0'..='9' => {
let digit = (c as u8 - b'0') as u64;
if significand.wrapping_mul(10).wrapping_add(digit) < u64::MAX {}
let _ = self.next();
significand = significand * 10 + digit;
}
_ => {
// return self.parse_number(positive, significand);
todo!()
}
}
}
}
_ => Err(Error::InvalidInteger),
}
}
fn consume_whitespace(&mut self) -> Result<()> {
loop {
match self.peek()? {
' ' | '\n' | '\t' | '\r' => {
let _ = self.next();
}
_ => {
return Ok(());
}
}
}
}
}
impl<'de, 'a> de::Deserializer<'de> for &'a mut Deserializer<'de> {
type Error = Error;
fn deserialize_any<V>(self, visitor: V) -> Result<V::Value>
where
V: Visitor<'de>,
{
match self.peek()? {
_ => todo!(),
}
}
fn deserialize_bool<V>(self, visitor: V) -> Result<V::Value>
where
V: Visitor<'de>,
{
visitor.visit_bool(self.parse_bool()?)
}
fn deserialize_i8<V>(self, visitor: V) -> Result<V::Value>
where
V: Visitor<'de>,
{
visitor.visit_i8(self.parse_signed()?)
}
fn deserialize_i16<V>(self, visitor: V) -> Result<V::Value>
where
V: Visitor<'de>,
{
visitor.visit_i16(self.parse_signed()?)
}
fn deserialize_i32<V>(self, visitor: V) -> Result<V::Value>
where
V: Visitor<'de>,
{
visitor.visit_i32(self.parse_signed()?)
}
fn deserialize_i64<V>(self, visitor: V) -> Result<V::Value>
where
V: Visitor<'de>,
{
visitor.visit_i64(self.parse_signed()?)
}
fn deserialize_u8<V>(self, visitor: V) -> Result<V::Value>
where
V: Visitor<'de>,
{
visitor.visit_u8(self.parse_unsigned()?)
}
fn deserialize_u16<V>(self, visitor: V) -> Result<V::Value>
where
V: Visitor<'de>,
{
visitor.visit_u16(self.parse_unsigned()?)
}
fn deserialize_u32<V>(self, visitor: V) -> Result<V::Value>
where
V: Visitor<'de>,
{
visitor.visit_u32(self.parse_unsigned()?)
}
fn deserialize_u64<V>(self, visitor: V) -> Result<V::Value>
where
V: Visitor<'de>,
{
visitor.visit_u64(self.parse_unsigned()?)
}
fn deserialize_f32<V>(self, visitor: V) -> Result<V::Value>
where
V: Visitor<'de>,
{
self.deserialize_f64(visitor)
}
fn deserialize_f64<V>(self, visitor: V) -> Result<V::Value>
where
V: Visitor<'de>,
{
unimplemented!("Cannot deserialize into a float value! Use a integer variant instead.")
}
fn deserialize_char<V>(self, visitor: V) -> Result<V::Value>
where
V: Visitor<'de>,
{
self.deserialize_str(visitor)
}
fn deserialize_str<V>(self, visitor: V) -> Result<V::Value>
where
V: Visitor<'de>,
{
todo!()
}
fn deserialize_string<V>(self, visitor: V) -> Result<V::Value>
where
V: Visitor<'de>,
{
self.deserialize_str(visitor)
}
fn deserialize_bytes<V>(self, visitor: V) -> Result<V::Value>
where
V: Visitor<'de>,
{
todo!()
}
fn deserialize_byte_buf<V>(self, visitor: V) -> Result<V::Value>
where
V: Visitor<'de>,
{
todo!()
}
fn deserialize_option<V>(self, visitor: V) -> Result<V::Value>
where
V: Visitor<'de>,
{
todo!()
}
fn deserialize_unit<V>(self, visitor: V) -> Result<V::Value>
where
V: Visitor<'de>,
{
visitor.visit_unit()
}
fn deserialize_unit_struct<V>(self, _name: &'static str, visitor: V) -> Result<V::Value>
where
V: Visitor<'de>,
{
visitor.visit_unit()
}
fn deserialize_newtype_struct<V>(self, name: &'static str, visitor: V) -> Result<V::Value>
where
V: Visitor<'de>,
{
visitor.visit_newtype_struct(self)
}
fn deserialize_seq<V>(self, visitor: V) -> Result<V::Value>
where
V: Visitor<'de>,
{
todo!()
}
fn deserialize_tuple<V>(self, _len: usize, visitor: V) -> Result<V::Value>
where
V: Visitor<'de>,
{
self.deserialize_seq(visitor)
}
fn deserialize_tuple_struct<V>(
self,
_name: &'static str,
_len: usize,
visitor: V,
) -> Result<V::Value>
where
V: Visitor<'de>,
{
self.deserialize_seq(visitor)
}
fn deserialize_map<V>(self, visitor: V) -> Result<V::Value>
where
V: Visitor<'de>,
{
self.consume_whitespace()?;
Ok(visitor.visit_map(self)?)
}
fn deserialize_struct<V>(
self,
_name: &'static str,
_fields: &'static [&'static str],
visitor: V,
) -> Result<V::Value>
where
V: Visitor<'de>,
{
visitor.visit_map(self)
}
fn deserialize_enum<V>(
self,
name: &'static str,
variants: &'static [&'static str],
visitor: V,
) -> Result<V::Value>
where
V: Visitor<'de>,
{
todo!()
}
fn deserialize_identifier<V>(self, visitor: V) -> Result<V::Value>
where
V: Visitor<'de>,
{
todo!()
}
fn deserialize_ignored_any<V>(self, visitor: V) -> Result<V::Value>
where
V: Visitor<'de>,
{
todo!()
}
}
impl<'de, 'a> MapAccess<'de> for Deserializer<'de> {
type Error = Error;
fn next_key_seed<K>(&mut self, seed: K) -> Result<Option<K::Value>>
where
K: DeserializeSeed<'de>,
{
// A map section is ended when another section begins or we hit EOL.
// Therefore, we only check if a next section begins or in the case of
// EOL indicate that we're done.
if self.peek().unwrap_or('[') == '[' {
return Ok(None);
}
seed.deserialize(self).map(Some)
}
fn next_value_seed<V>(&mut self, seed: V) -> Result<V::Value>
where
V: DeserializeSeed<'de>,
{
seed.deserialize(self)
}
}
#[cfg(test)]
mod deserialize {
use crate::from_str;
use serde_derive::Deserialize;
#[test]
fn unit() {
#[derive(Deserialize, PartialEq, Debug)]
struct Test;
assert_eq!(Test, from_str("").unwrap());
assert_eq!((), from_str("").unwrap());
}
#[test]
#[should_panic]
fn float() {
from_str::<f64>("").unwrap();
}
#[test]
fn basic() {
#[derive(Deserialize, PartialEq, Debug)]
struct Config {
user: User,
}
#[derive(Deserialize, PartialEq, Debug)]
struct User {
email: String,
name: String,
}
let expected = Config {
user: User {
email: "code@eddie.sh".to_string(),
name: "Edward Shen".to_string(),
},
};
assert_eq!(
expected,
from_str("[user]\nemail=code@eddie.sh\nname=Edward Shen\n").unwrap()
);
}
}

View file

@ -1,42 +0,0 @@
use std::fmt::{self, Display};
use serde::{de, ser};
pub type Result<T> = std::result::Result<T, Error>;
// This is a bare-bones implementation. A real library would provide additional
// information in its error type, for example the line and column at which the
// error occurred, the byte offset into the input, or the current key being
// processed.
#[derive(Clone, Debug, PartialEq)]
pub enum Error {
Message(String),
Eof,
InvalidInteger,
InvalidBoolean(String),
}
impl ser::Error for Error {
fn custom<T: Display>(msg: T) -> Self {
Error::Message(msg.to_string())
}
}
impl de::Error for Error {
fn custom<T: Display>(msg: T) -> Self {
Error::Message(msg.to_string())
}
}
impl Display for Error {
fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
match self {
Error::Message(msg) => formatter.write_str(msg),
Error::Eof => formatter.write_str("unexpected end of input"),
Error::InvalidInteger => formatter.write_str("invalid integer given"),
Error::InvalidBoolean(_) => formatter.write_str("invalid boolean given"),
}
}
}
impl std::error::Error for Error {}

2557
src/file.rs Normal file

File diff suppressed because it is too large Load diff

View file

@ -1,10 +1,70 @@
// mod de;
pub mod config;
mod error;
// mod ser;
#![forbid(unsafe_code)]
#![deny(missing_docs)]
#![warn(clippy::pedantic, clippy::nursery, clippy::cargo)]
//! # `git_config`
//!
//! This crate is a high performance `git-config` file reader and writer. It
//! exposes a high level API to parse, read, and write [`git-config` files],
//! which are loosely based on the [INI file format].
//!
//! This crate has a few primary offerings and various accessory functions. The
//! table below gives a brief explanation of all offerings, loosely in order
//! from the highest to lowest abstraction.
//!
//! | Offering | Description | Zero-copy? |
//! | ------------- | --------------------------------------------------- | ----------------- |
//! | [`GitConfig`] | Accelerated wrapper for reading and writing values. | On some reads[^1] |
//! | [`Parser`] | Syntactic event emitter for `git-config` files. | Yes |
//! | [`Value`] | Wrappers for `git-config` value types. | Yes |
//!
//! This crate also exposes efficient value normalization which unescapes
//! characters and removes quotes through the `normalize_*` family of functions,
//! located in the [`values`] module.
//!
//! # Zero-copy versus zero-alloc
//!
//! We follow [`nom`]'s definition of "zero-copy":
//!
//! > If a parser returns a subset of its input data, it will return a slice of
//! > that input, without copying.
//!
//! Due to the syntax of `git-config`, we must allocate at the parsing level
//! (and thus higher level abstractions must allocate as well) in order to
//! provide a meaningful event stream. That being said, all operations with the
//! parser is still zero-copy. Higher level abstractions may have operations
//! that are zero-copy, but are not guaranteed to do so.
//!
//! However, we intend to be performant as possible, so allocations are
//! limited restricted and we attempt to avoid copying whenever possible.
//!
//! [^1]: When read values do not need normalization.
//!
//! [`git-config` files]: https://git-scm.com/docs/git-config#_configuration_file
//! [INI file format]: https://en.wikipedia.org/wiki/INI_file
//! [`GitConfig`]: crate::file::GitConfig
//! [`Parser`]: crate::parser::Parser
//! [`Value`]: crate::values::Value
//! [`values`]: crate::values
//! [`nom`]: https://github.com/Geal/nom
// Cargo.toml cannot have self-referential dependencies, so you can't just
// specify the actual serde crate when you define a feature called serde. We
// instead call the serde crate as serde_crate and then rename the crate to
// serde, to get around this in an intuitive manner.
#[cfg(feature = "serde")]
extern crate serde_crate as serde;
pub mod file;
pub mod parser;
pub mod values;
// mod de;
// mod ser;
// mod error;
// pub use de::{from_str, Deserializer};
pub use error::{Error, Result};
// pub use error::{Error, Result};
// pub use ser::{to_string, Serializer};
#[cfg(test)]
pub mod test_util;

File diff suppressed because it is too large Load diff

View file

@ -1,184 +0,0 @@
use crate::error::{Error, Result};
use ser::SerializeSeq;
use serde::{de::MapAccess, ser, Serialize};
pub struct Serializer {
output: String,
}
pub fn to_string<T>(value: &T) -> Result<String>
where
T: Serialize,
{
let mut serializer = Serializer {
output: String::new(),
};
value.serialize(&mut serializer)?;
Ok(serializer.output)
}
impl<'a> ser::Serializer for &'a mut Serializer {
type Ok = ();
type Error = Error;
type SerializeSeq = Self;
type SerializeTuple = Self;
type SerializeTupleStruct = Self;
type SerializeTupleVariant = Self;
type SerializeMap = Self;
type SerializeStruct = Self;
type SerializeStructVariant = Self;
fn serialize_bool(self, v: bool) -> Result<Self::Ok> {
self.output += if v { "true" } else { "false" };
Ok(())
}
fn serialize_i8(self, v: i8) -> Result<Self::Ok> {
self.serialize_i64(i64::from(v))
}
fn serialize_i16(self, v: i16) -> Result<Self::Ok> {
self.serialize_i64(i64::from(v))
}
fn serialize_i32(self, v: i32) -> Result<Self::Ok> {
self.serialize_i64(i64::from(v))
}
fn serialize_i64(self, v: i64) -> Result<Self::Ok> {
self.output += &v.to_string();
Ok(())
}
fn serialize_u8(self, v: u8) -> Result<Self::Ok> {
self.serialize_u64(u64::from(v))
}
fn serialize_u16(self, v: u16) -> Result<Self::Ok> {
self.serialize_u64(u64::from(v))
}
fn serialize_u32(self, v: u32) -> Result<Self::Ok> {
self.serialize_u64(u64::from(v))
}
fn serialize_u64(self, v: u64) -> Result<Self::Ok> {
self.output += &v.to_string();
Ok(())
}
fn serialize_f32(self, v: f32) -> Result<Self::Ok> {
self.serialize_f64(f64::from(v))
}
fn serialize_f64(self, v: f64) -> Result<Self::Ok> {
self.output += &v.to_string();
Ok(())
}
fn serialize_char(self, v: char) -> Result<Self::Ok> {
self.output += &v.to_string();
Ok(())
}
fn serialize_str(self, v: &str) -> Result<Self::Ok> {
self.output += v;
Ok(())
}
fn serialize_bytes(self, v: &[u8]) -> Result<Self::Ok> {
todo!()
}
fn serialize_none(self) -> Result<Self::Ok> {
todo!()
}
fn serialize_some<T: ?Sized>(self, value: &T) -> Result<Self::Ok>
where
T: Serialize,
{
todo!()
}
fn serialize_unit(self) -> Result<Self::Ok> {
todo!()
}
fn serialize_unit_struct(self, name: &'static str) -> Result<Self::Ok> {
todo!()
}
fn serialize_unit_variant(
self,
name: &'static str,
variant_index: u32,
variant: &'static str,
) -> Result<Self::Ok> {
todo!()
}
fn serialize_newtype_struct<T: ?Sized>(self, name: &'static str, value: &T) -> Result<Self::Ok>
where
T: Serialize,
{
todo!()
}
fn serialize_newtype_variant<T: ?Sized>(
self,
name: &'static str,
variant_index: u32,
variant: &'static str,
value: &T,
) -> Result<Self::Ok>
where
T: Serialize,
{
todo!()
}
fn serialize_seq(self, len: Option<usize>) -> Result<Self::SerializeSeq> {
todo!()
}
fn serialize_tuple(self, len: usize) -> Result<Self::SerializeTuple> {
self.serialize_seq(Some(len))
}
fn serialize_tuple_struct(
self,
name: &'static str,
len: usize,
) -> Result<Self::SerializeTupleStruct> {
self.serialize_seq(Some(len))
}
fn serialize_tuple_variant(
self,
name: &'static str,
variant_index: u32,
variant: &'static str,
len: usize,
) -> Result<Self::SerializeTupleVariant> {
todo!()
}
fn serialize_map(self, len: Option<usize>) -> Result<Self::SerializeMap> {
todo!()
}
fn serialize_struct(self, name: &'static str, len: usize) -> Result<Self::SerializeStruct> {
todo!()
}
fn serialize_struct_variant(
self,
name: &'static str,
variant_index: u32,
variant: &'static str,
len: usize,
) -> Result<Self::SerializeStructVariant> {
todo!()
}
}

75
src/test_util.rs Normal file
View file

@ -0,0 +1,75 @@
//! This module is only included for tests, and contains common unit test helper
//! functions.
use crate::parser::{Event, Key, ParsedComment, ParsedSectionHeader};
use std::borrow::Cow;
pub fn section_header_event(
name: &str,
subsection: impl Into<Option<(&'static str, &'static str)>>,
) -> Event<'_> {
Event::SectionHeader(section_header(name, subsection))
}
pub fn section_header(
name: &str,
subsection: impl Into<Option<(&'static str, &'static str)>>,
) -> ParsedSectionHeader<'_> {
let name = name.into();
if let Some((separator, subsection_name)) = subsection.into() {
ParsedSectionHeader {
name,
separator: Some(Cow::Borrowed(separator)),
subsection_name: Some(Cow::Borrowed(subsection_name)),
}
} else {
ParsedSectionHeader {
name,
separator: None,
subsection_name: None,
}
}
}
pub(crate) fn name_event(name: &'static str) -> Event<'static> {
Event::Key(Key(Cow::Borrowed(name)))
}
pub(crate) fn value_event(value: &'static str) -> Event<'static> {
Event::Value(Cow::Borrowed(value.as_bytes()))
}
pub(crate) fn value_not_done_event(value: &'static str) -> Event<'static> {
Event::ValueNotDone(Cow::Borrowed(value.as_bytes()))
}
pub(crate) fn value_done_event(value: &'static str) -> Event<'static> {
Event::ValueDone(Cow::Borrowed(value.as_bytes()))
}
pub(crate) fn newline_event() -> Event<'static> {
newline_custom_event("\n")
}
pub(crate) fn newline_custom_event(value: &'static str) -> Event<'static> {
Event::Newline(Cow::Borrowed(value))
}
pub(crate) fn whitespace_event(value: &'static str) -> Event<'static> {
Event::Whitespace(Cow::Borrowed(value))
}
pub(crate) fn comment_event(tag: char, msg: &'static str) -> Event<'static> {
Event::Comment(comment(tag, msg))
}
pub(crate) fn comment(comment_tag: char, comment: &'static str) -> ParsedComment<'static> {
ParsedComment {
comment_tag,
comment: Cow::Borrowed(comment.as_bytes()),
}
}
pub(crate) fn fully_consumed<T>(t: T) -> (&'static [u8], T) {
(&[], t)
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,127 @@
use git_config::file::GitConfig;
use git_config::values::*;
use std::borrow::Cow;
use std::convert::TryFrom;
/// Asserts we can cast into all variants of our type
#[test]
fn get_value_for_all_provided_values() -> Result<(), Box<dyn std::error::Error>> {
let config = r#"
[core]
bool-explicit = false
bool-implicit
integer-no-prefix = 10
integer-prefix = 10g
color = brightgreen red \
bold
other = hello world
"#;
let file = GitConfig::try_from(config)?;
assert_eq!(
file.value::<Boolean>("core", None, "bool-explicit")?,
Boolean::False(Cow::Borrowed("false"))
);
assert_eq!(
file.value::<Boolean>("core", None, "bool-implicit")?,
Boolean::True(TrueVariant::Implicit)
);
assert_eq!(
file.value::<Integer>("core", None, "integer-no-prefix")?,
Integer {
value: 10,
suffix: None
}
);
assert_eq!(
file.value::<Integer>("core", None, "integer-no-prefix")?,
Integer {
value: 10,
suffix: None
}
);
assert_eq!(
file.value::<Integer>("core", None, "integer-prefix")?,
Integer {
value: 10,
suffix: Some(IntegerSuffix::Gibi),
}
);
assert_eq!(
file.value::<Color>("core", None, "color")?,
Color {
foreground: Some(ColorValue::BrightGreen),
background: Some(ColorValue::Red),
attributes: vec![ColorAttribute::Bold]
}
);
assert_eq!(
file.value::<Value>("core", None, "other")?,
Value::Other(Cow::Borrowed(b"hello world"))
);
Ok(())
}
/// There was a regression where lookup would fail because we only checked the
/// last section entry for any given section and subsection
#[test]
fn get_value_looks_up_all_sections_before_failing() -> Result<(), Box<dyn std::error::Error>> {
let config = r#"
[core]
bool-explicit = false
bool-implicit = false
[core]
bool-implicit
"#;
let file = GitConfig::try_from(config)?;
// Checks that we check the last entry first still
assert_eq!(
file.value::<Boolean>("core", None, "bool-implicit")?,
Boolean::True(TrueVariant::Implicit)
);
assert_eq!(
file.value::<Boolean>("core", None, "bool-explicit")?,
Boolean::False(Cow::Borrowed("false"))
);
Ok(())
}
#[test]
fn section_names_are_case_insensitive() -> Result<(), Box<dyn std::error::Error>> {
let config = "[core] bool-implicit";
let file = GitConfig::try_from(config)?;
assert!(file.value::<Boolean>("core", None, "bool-implicit").is_ok());
assert_eq!(
file.value::<Boolean>("core", None, "bool-implicit"),
file.value::<Boolean>("CORE", None, "bool-implicit")
);
Ok(())
}
#[test]
fn value_names_are_case_insensitive() -> Result<(), Box<dyn std::error::Error>> {
let config = "[core]
a = true
A = false";
let file = GitConfig::try_from(config)?;
assert_eq!(file.multi_value::<Boolean>("core", None, "a")?.len(), 2);
assert_eq!(
file.value::<Boolean>("core", None, "a"),
file.value::<Boolean>("core", None, "A")
);
Ok(())
}

View file

@ -0,0 +1,8 @@
// See https://matklad.github.io/2021/02/27/delete-cargo-integration-tests.html
// for an explanation of why the integration tests are laid out like this.
//
// TL;DR single mod makes integration tests faster to compile, test, and with
// less build artifacts.
mod file_integeration_test;
mod parser_integration_tests;

View file

@ -0,0 +1,227 @@
use git_config::parser::{parse_from_str, Event, Key, ParsedSectionHeader, SectionHeaderName};
use std::borrow::Cow;
pub fn section_header_event(
name: &str,
subsection: impl Into<Option<(&'static str, &'static str)>>,
) -> Event<'_> {
Event::SectionHeader(section_header(name, subsection))
}
pub fn section_header(
name: &str,
subsection: impl Into<Option<(&'static str, &'static str)>>,
) -> ParsedSectionHeader<'_> {
let name = SectionHeaderName(Cow::Borrowed(name));
if let Some((separator, subsection_name)) = subsection.into() {
ParsedSectionHeader {
name,
separator: Some(Cow::Borrowed(separator)),
subsection_name: Some(Cow::Borrowed(subsection_name)),
}
} else {
ParsedSectionHeader {
name,
separator: None,
subsection_name: None,
}
}
}
fn name(name: &'static str) -> Event<'static> {
Event::Key(Key(Cow::Borrowed(name)))
}
fn value(value: &'static str) -> Event<'static> {
Event::Value(Cow::Borrowed(value.as_bytes()))
}
fn newline() -> Event<'static> {
newline_custom("\n")
}
fn newline_custom(value: &'static str) -> Event<'static> {
Event::Newline(Cow::Borrowed(value))
}
fn whitespace(value: &'static str) -> Event<'static> {
Event::Whitespace(Cow::Borrowed(value))
}
fn separator() -> Event<'static> {
Event::KeyValueSeparator
}
#[test]
#[rustfmt::skip]
fn personal_config() {
let config = r#"[user]
email = code@eddie.sh
name = Foo Bar
[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!(
parse_from_str(config)
.unwrap()
.into_vec(),
vec![
section_header_event("user", None),
newline(),
whitespace(" "),
name("email"),
whitespace(" "),
separator(),
whitespace(" "),
value("code@eddie.sh"),
newline(),
whitespace(" "),
name("name"),
whitespace(" "),
separator(),
whitespace(" "),
value("Foo Bar"),
newline(),
section_header_event("core", None),
newline(),
whitespace(" "),
name("autocrlf"),
whitespace(" "),
separator(),
whitespace(" "),
value("input"),
newline(),
section_header_event("push", None),
newline(),
whitespace(" "),
name("default"),
whitespace(" "),
separator(),
whitespace(" "),
value("simple"),
newline(),
section_header_event("commit", None),
newline(),
whitespace(" "),
name("gpgsign"),
whitespace(" "),
separator(),
whitespace(" "),
value("true"),
newline(),
section_header_event("gpg", None),
newline(),
whitespace(" "),
name("program"),
whitespace(" "),
separator(),
whitespace(" "),
value("gpg"),
newline(),
section_header_event("url", (" ", "ssh://git@github.com/")),
newline(),
whitespace(" "),
name("insteadOf"),
whitespace(" "),
separator(),
whitespace(" "),
value("\"github://\""),
newline(),
section_header_event("url", (" ", "ssh://git@git.eddie.sh/edward/")),
newline(),
whitespace(" "),
name("insteadOf"),
whitespace(" "),
separator(),
whitespace(" "),
value("\"gitea://\""),
newline(),
section_header_event("pull", None),
newline(),
whitespace(" "),
name("ff"),
whitespace(" "),
separator(),
whitespace(" "),
value("only"),
newline(),
section_header_event("init", None),
newline(),
whitespace(" "),
name("defaultBranch"),
whitespace(" "),
separator(),
whitespace(" "),
value("master"),
]
);
}
#[test]
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![newline_custom("\n\n\n\n\n")]
);
}
#[test]
fn error() {
let input = "[core] a=b\n 4a=3";
println!("{}", parse_from_str(input).unwrap_err());
let input = "[core] a=b\n =3";
println!("{}", parse_from_str(input).unwrap_err());
let input = "[core";
println!("{}", parse_from_str(input).unwrap_err());
}

View file

@ -1,82 +0,0 @@
use serde_git_config::parser::{parse_from_str, Event, ParsedSectionHeader, Parser};
use serde_git_config::values::Value;
fn fully_consumed<T>(t: T) -> (&'static str, T) {
("", t)
}
fn section_header(name: &'static str, subname: impl Into<Option<&'static str>>) -> Event<'static> {
Event::SectionHeader(ParsedSectionHeader {
name,
subsection_name: subname.into(),
})
}
fn name(name: &'static str) -> Event<'static> {
Event::Key(name)
}
fn value(value: &'static str) -> Event<'static> {
Event::Value(Value::from_str(value))
}
#[test]
fn personal_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!(
parse_from_str(config)
.unwrap()
.into_iter()
.collect::<Vec<_>>(),
vec![
section_header("user", None),
name("email"),
value("code@eddie.sh"),
name("name"),
value("Edward Shen"),
section_header("core", None),
name("autocrlf"),
value("input"),
section_header("push", None),
name("default"),
value("simple"),
section_header("commit", None),
name("gpgsign"),
value("true"),
section_header("gpg", None),
name("program"),
value("gpg"),
section_header("url", "ssh://git@github.com/"),
name("insteadOf"),
value("\"github://\""),
section_header("url", "ssh://git@git.eddie.sh/edward/"),
name("insteadOf"),
value("\"gitea://\""),
section_header("pull", None),
name("ff"),
value("only"),
section_header("init", None),
name("defaultBranch"),
value("master"),
]
);
}