D7575: hg-core: vendor Facebook's configparser crate

indygreg (Gregory Szorc) phabricator at mercurial-scm.org
Sat Dec 7 20:19:34 UTC 2019


indygreg created this revision.
Herald added subscribers: mercurial-devel, mjpieters, kevincox, durin42.
Herald added a reviewer: hg-reviewers.

REVISION SUMMARY
  I added a number of files from https://github.com/facebookexperimental/eden
  at commit b745b4421b8a8b130d2094b598cedf65655410ec. Files are unmodified from
  their original versions. However, the paths are different: I've put all
  files in the same directory. And I've removed some files we don't care about
  (such as the C++ bindings and CMake rules).
  
  The added files are not part of the hg-core project yet. Things will be
  incorporated in future commits.
  
  The added generate_parser.py does make check-code unhappy. This will
  be fixed in a subsequent commit.
  
  I haven't extensively audited the added code for functional correctness
  and compatibility. But I did skim it and it seems to be a highly compatible
  config parsing implementation. The most suspect code I found was around
  config file path handling. There are references to "tupperware," which
  appears to be a Facebook-specific thing. We will want to clean this up
  at some point...

REPOSITORY
  rHG Mercurial

BRANCH
  default

REVISION DETAIL
  https://phab.mercurial-scm.org/D7575

AFFECTED FILES
  rust/hg-core/src/configparser/c_api.rs
  rust/hg-core/src/configparser/config.rs
  rust/hg-core/src/configparser/error.rs
  rust/hg-core/src/configparser/generate_parser.py
  rust/hg-core/src/configparser/hg.rs
  rust/hg-core/src/configparser/lib.rs
  rust/hg-core/src/configparser/parser.rs
  rust/hg-core/src/configparser/spec.pest

CHANGE DETAILS

diff --git a/rust/hg-core/src/configparser/spec.pest b/rust/hg-core/src/configparser/spec.pest
new file mode 100644
--- /dev/null
+++ b/rust/hg-core/src/configparser/spec.pest
@@ -0,0 +1,63 @@
+// "comment" and "whitespace" have special meaning in pest. They cause more
+// trouble than benefit here. Therfore, avoid them.
+// See https://pest-parser.github.io/book/grammars/syntax.html
+//
+// Names are used in error messages. Certain rules are used to improved UX.
+// For example,
+//
+//    equal_sign = { space* ~ "=" ~ space* }
+//    config_item = { name ~ equal_sign ~ value }
+//
+// is more friendly than:
+//
+//    config_item = { name ~ space* ~ "=" ~ space* ~ value }
+//
+// because the former shows "expect space", while the latter shows
+// "expect equal_sign", for the following illegal content:
+//
+//    [section]
+//    lack-of-equal-sign
+//                      ^ error shows here
+//
+// Same applies to "directive" and "bracket"s.
+
+
+new_line = { "\n" | "\r\n" }
+space = { " " | "\t" }
+comment_start = { ("#" | ";") }
+
+line = @{ (!new_line ~ ANY)* }
+
+value = ${ line ~ (new_line ~ space+ ~ line)* }
+equal_sign = @{ "=" ~ space* }
+
+// Excluding special prefixes explicitly from config_name affects error
+// messages. For example:
+//
+//    []
+//     ^ expect section_name (with "[" excluded)
+//      ^ except equal_sign (without "[" excluded)
+//
+//    %unknown
+//     ^ expect unset or include (with "%" excluded)
+//            ^ expect equal_sign (without "%" excluded)
+//
+// The "expect equal_sign" version is less friendly.
+config_name = @{ !("[" | "=" | "%" | space | comment_start | new_line) ~ ANY ~ (!("=" | new_line) ~ ANY)* }
+config_item = ${ config_name ~ equal_sign ~ value }
+
+left_bracket = @{ "[" }
+right_bracket = @{ "]" }
+
+section_name = @{ (!("]" | new_line) ~ ANY)+ }
+section = ${ left_bracket ~ section_name ~ right_bracket ~ space* }
+
+comment_line = @{ comment_start ~ line }
+blank_line = @{ space* }
+
+directive = ${ "%" ~ (include | unset) }
+include = ${ "include" ~ space+ ~ line }
+unset = ${ "unset" ~ space+ ~ config_name ~ space* }
+
+compound = _{ (config_item | section | comment_line | directive | blank_line ) }
+file = _{ SOI ~ compound ~ (new_line ~ compound)* ~ EOI }
diff --git a/rust/hg-core/src/configparser/parser.rs b/rust/hg-core/src/configparser/parser.rs
new file mode 100644
--- /dev/null
+++ b/rust/hg-core/src/configparser/parser.rs
@@ -0,0 +1,651 @@
+// Copyright 2018 Facebook, Inc.
+//
+// This software may be used and distributed according to the terms of the
+// GNU General Public License version 2 or any later version.
+
+
+// Generated by generate_parser.py. Do not edit manually. Instead, edit
+// spec.pest, then run generate_parser.py (require cargo-expand).
+//
+// This file should really be just 3 lines:
+//
+// #[derive(Parser)]
+// #[grammar = "spec.pest"]
+// pub(crate) struct ConfigParser;
+//
+// However, `#[grammar = "spec.pest"]` does not play well with Buck build,
+// because pest_derive cannot find "spec.pest" in buck build environment.
+// Therefore this file is @generated. @no-lint.
+// pest-checksum: af79d1287503f19c331643cd2e28d30f6312592f.
+
+
+#[allow(dead_code, non_camel_case_types)]
+#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
+pub enum Rule {
+    EOI,
+    new_line,
+    space,
+    comment_start,
+    line,
+    value,
+    equal_sign,
+    config_name,
+    config_item,
+    left_bracket,
+    right_bracket,
+    section_name,
+    section,
+    comment_line,
+    blank_line,
+    directive,
+    include,
+    unset,
+    compound,
+    file,
+}
+
+pub(crate) struct ConfigParser;
+
+impl ::pest::Parser<Rule> for ConfigParser {
+    fn parse<'i>(rule: Rule, input: &'i str)
+     ->
+         ::std::result::Result<::pest::iterators::Pairs<'i, Rule>,
+                               ::pest::error::Error<Rule>> {
+        mod rules {
+            pub mod hidden {
+                use super::super::Rule;
+                #[inline]
+                #[allow(dead_code, non_snake_case, unused_variables)]
+                pub fn skip(state: Box<::pest::ParserState<Rule>>)
+                 -> ::pest::ParseResult<Box<::pest::ParserState<Rule>>> {
+                    Ok(state)
+                }
+            }
+            pub mod visible {
+                use super::super::Rule;
+                #[inline]
+                #[allow(non_snake_case, unused_variables)]
+                pub fn new_line(state: Box<::pest::ParserState<Rule>>)
+                 -> ::pest::ParseResult<Box<::pest::ParserState<Rule>>> {
+                    state.rule(Rule::new_line,
+                               |state|
+                                   {
+                                       state.match_string("\n").or_else(|state|
+                                                                            {
+                                                                                state.match_string("\r\n")
+                                                                            })
+                                   })
+                }
+                #[inline]
+                #[allow(non_snake_case, unused_variables)]
+                pub fn space(state: Box<::pest::ParserState<Rule>>)
+                 -> ::pest::ParseResult<Box<::pest::ParserState<Rule>>> {
+                    state.rule(Rule::space,
+                               |state|
+                                   {
+                                       state.match_string(" ").or_else(|state|
+                                                                           {
+                                                                               state.match_string("\t")
+                                                                           })
+                                   })
+                }
+                #[inline]
+                #[allow(non_snake_case, unused_variables)]
+                pub fn comment_start(state: Box<::pest::ParserState<Rule>>)
+                 -> ::pest::ParseResult<Box<::pest::ParserState<Rule>>> {
+                    state.rule(Rule::comment_start,
+                               |state|
+                                   {
+                                       state.match_string("#").or_else(|state|
+                                                                           {
+                                                                               state.match_string(";")
+                                                                           })
+                                   })
+                }
+                #[inline]
+                #[allow(non_snake_case, unused_variables)]
+                pub fn line(state: Box<::pest::ParserState<Rule>>)
+                 -> ::pest::ParseResult<Box<::pest::ParserState<Rule>>> {
+                    state.rule(Rule::line,
+                               |state|
+                                   {
+                                       state.atomic(::pest::Atomicity::Atomic,
+                                                    |state|
+                                                        {
+                                                            state.repeat(|state|
+                                                                             {
+                                                                                 state.sequence(|state|
+                                                                                                    {
+                                                                                                        state.lookahead(false,
+                                                                                                                        |state|
+                                                                                                                            {
+                                                                                                                                self::new_line(state)
+                                                                                                                            }).and_then(|state|
+                                                                                                                                            {
+                                                                                                                                                self::ANY(state)
+                                                                                                                                            })
+                                                                                                    })
+                                                                             })
+                                                        })
+                                   })
+                }
+                #[inline]
+                #[allow(non_snake_case, unused_variables)]
+                pub fn value(state: Box<::pest::ParserState<Rule>>)
+                 -> ::pest::ParseResult<Box<::pest::ParserState<Rule>>> {
+                    state.atomic(::pest::Atomicity::CompoundAtomic,
+                                 |state|
+                                     {
+                                         state.rule(Rule::value,
+                                                    |state|
+                                                        {
+                                                            state.sequence(|state|
+                                                                               {
+                                                                                   self::line(state).and_then(|state|
+                                                                                                                  {
+                                                                                                                      state.repeat(|state|
+                                                                                                                                       {
+                                                                                                                                           state.sequence(|state|
+                                                                                                                                                              {
+                                                                                                                                                                  self::new_line(state).and_then(|state|
+                                                                                                                                                                                                     {
+                                                                                                                                                                                                         state.sequence(|state|
+                                                                                                                                                                                                                            {
+                                                                                                                                                                                                                                self::space(state).and_then(|state|
+                                                                                                                                                                                                                                                                {
+                                                                                                                                                                                                                                                                    state.repeat(|state|
+                                                                                                                                                                                                                                                                                     {
+                                                                                                                                                                                                                                                                                         self::space(state)
+                                                                                                                                                                                                                                                                                     })
+                                                                                                                                                                                                                                                                })
+                                                                                                                                                                                                                            })
+                                                                                                                                                                                                     }).and_then(|state|
+                                                                                                                                                                                                                     {
+                                                                                                                                                                                                                         self::line(state)
+                                                                                                                                                                                                                     })
+                                                                                                                                                              })
+                                                                                                                                       })
+                                                                                                                  })
+                                                                               })
+                                                        })
+                                     })
+                }
+                #[inline]
+                #[allow(non_snake_case, unused_variables)]
+                pub fn equal_sign(state: Box<::pest::ParserState<Rule>>)
+                 -> ::pest::ParseResult<Box<::pest::ParserState<Rule>>> {
+                    state.rule(Rule::equal_sign,
+                               |state|
+                                   {
+                                       state.atomic(::pest::Atomicity::Atomic,
+                                                    |state|
+                                                        {
+                                                            state.sequence(|state|
+                                                                               {
+                                                                                   state.match_string("=").and_then(|state|
+                                                                                                                        {
+                                                                                                                            state.repeat(|state|
+                                                                                                                                             {
+                                                                                                                                                 self::space(state)
+                                                                                                                                             })
+                                                                                                                        })
+                                                                               })
+                                                        })
+                                   })
+                }
+                #[inline]
+                #[allow(non_snake_case, unused_variables)]
+                pub fn config_name(state: Box<::pest::ParserState<Rule>>)
+                 -> ::pest::ParseResult<Box<::pest::ParserState<Rule>>> {
+                    state.rule(Rule::config_name,
+                               |state|
+                                   {
+                                       state.atomic(::pest::Atomicity::Atomic,
+                                                    |state|
+                                                        {
+                                                            state.sequence(|state|
+                                                                               {
+                                                                                   state.lookahead(false,
+                                                                                                   |state|
+                                                                                                       {
+                                                                                                           state.match_string("[").or_else(|state|
+                                                                                                                                               {
+                                                                                                                                                   state.match_string("=")
+                                                                                                                                               }).or_else(|state|
+                                                                                                                                                              {
+                                                                                                                                                                  state.match_string("%")
+                                                                                                                                                              }).or_else(|state|
+                                                                                                                                                                             {
+                                                                                                                                                                                 self::space(state)
+                                                                                                                                                                             }).or_else(|state|
+                                                                                                                                                                                            {
+                                                                                                                                                                                                self::comment_start(state)
+                                                                                                                                                                                            }).or_else(|state|
+                                                                                                                                                                                                           {
+                                                                                                                                                                                                               self::new_line(state)
+                                                                                                                                                                                                           })
+                                                                                                       }).and_then(|state|
+                                                                                                                       {
+                                                                                                                           self::ANY(state)
+                                                                                                                       }).and_then(|state|
+                                                                                                                                       {
+                                                                                                                                           state.repeat(|state|
+                                                                                                                                                            {
+                                                                                                                                                                state.sequence(|state|
+                                                                                                                                                                                   {
+                                                                                                                                                                                       state.lookahead(false,
+                                                                                                                                                                                                       |state|
+                                                                                                                                                                                                           {
+                                                                                                                                                                                                               state.match_string("=").or_else(|state|
+                                                                                                                                                                                                                                                   {
+                                                                                                                                                                                                                                                       self::new_line(state)
+                                                                                                                                                                                                                                                   })
+                                                                                                                                                                                                           }).and_then(|state|
+                                                                                                                                                                                                                           {
+                                                                                                                                                                                                                               self::ANY(state)
+                                                                                                                                                                                                                           })
+                                                                                                                                                                                   })
+                                                                                                                                                            })
+                                                                                                                                       })
+                                                                               })
+                                                        })
+                                   })
+                }
+                #[inline]
+                #[allow(non_snake_case, unused_variables)]
+                pub fn config_item(state: Box<::pest::ParserState<Rule>>)
+                 -> ::pest::ParseResult<Box<::pest::ParserState<Rule>>> {
+                    state.atomic(::pest::Atomicity::CompoundAtomic,
+                                 |state|
+                                     {
+                                         state.rule(Rule::config_item,
+                                                    |state|
+                                                        {
+                                                            state.sequence(|state|
+                                                                               {
+                                                                                   self::config_name(state).and_then(|state|
+                                                                                                                         {
+                                                                                                                             self::equal_sign(state)
+                                                                                                                         }).and_then(|state|
+                                                                                                                                         {
+                                                                                                                                             self::value(state)
+                                                                                                                                         })
+                                                                               })
+                                                        })
+                                     })
+                }
+                #[inline]
+                #[allow(non_snake_case, unused_variables)]
+                pub fn left_bracket(state: Box<::pest::ParserState<Rule>>)
+                 -> ::pest::ParseResult<Box<::pest::ParserState<Rule>>> {
+                    state.rule(Rule::left_bracket,
+                               |state|
+                                   {
+                                       state.atomic(::pest::Atomicity::Atomic,
+                                                    |state|
+                                                        {
+                                                            state.match_string("[")
+                                                        })
+                                   })
+                }
+                #[inline]
+                #[allow(non_snake_case, unused_variables)]
+                pub fn right_bracket(state: Box<::pest::ParserState<Rule>>)
+                 -> ::pest::ParseResult<Box<::pest::ParserState<Rule>>> {
+                    state.rule(Rule::right_bracket,
+                               |state|
+                                   {
+                                       state.atomic(::pest::Atomicity::Atomic,
+                                                    |state|
+                                                        {
+                                                            state.match_string("]")
+                                                        })
+                                   })
+                }
+                #[inline]
+                #[allow(non_snake_case, unused_variables)]
+                pub fn section_name(state: Box<::pest::ParserState<Rule>>)
+                 -> ::pest::ParseResult<Box<::pest::ParserState<Rule>>> {
+                    state.rule(Rule::section_name,
+                               |state|
+                                   {
+                                       state.atomic(::pest::Atomicity::Atomic,
+                                                    |state|
+                                                        {
+                                                            state.sequence(|state|
+                                                                               {
+                                                                                   state.sequence(|state|
+                                                                                                      {
+                                                                                                          state.lookahead(false,
+                                                                                                                          |state|
+                                                                                                                              {
+                                                                                                                                  state.match_string("]").or_else(|state|
+                                                                                                                                                                      {
+                                                                                                                                                                          self::new_line(state)
+                                                                                                                                                                      })
+                                                                                                                              }).and_then(|state|
+                                                                                                                                              {
+                                                                                                                                                  self::ANY(state)
+                                                                                                                                              })
+                                                                                                      }).and_then(|state|
+                                                                                                                      {
+                                                                                                                          state.repeat(|state|
+                                                                                                                                           {
+                                                                                                                                               state.sequence(|state|
+                                                                                                                                                                  {
+                                                                                                                                                                      state.lookahead(false,
+                                                                                                                                                                                      |state|
+                                                                                                                                                                                          {
+                                                                                                                                                                                              state.match_string("]").or_else(|state|
+                                                                                                                                                                                                                                  {
+                                                                                                                                                                                                                                      self::new_line(state)
+                                                                                                                                                                                                                                  })
+                                                                                                                                                                                          }).and_then(|state|
+                                                                                                                                                                                                          {
+                                                                                                                                                                                                              self::ANY(state)
+                                                                                                                                                                                                          })
+                                                                                                                                                                  })
+                                                                                                                                           })
+                                                                                                                      })
+                                                                               })
+                                                        })
+                                   })
+                }
+                #[inline]
+                #[allow(non_snake_case, unused_variables)]
+                pub fn section(state: Box<::pest::ParserState<Rule>>)
+                 -> ::pest::ParseResult<Box<::pest::ParserState<Rule>>> {
+                    state.atomic(::pest::Atomicity::CompoundAtomic,
+                                 |state|
+                                     {
+                                         state.rule(Rule::section,
+                                                    |state|
+                                                        {
+                                                            state.sequence(|state|
+                                                                               {
+                                                                                   self::left_bracket(state).and_then(|state|
+                                                                                                                          {
+                                                                                                                              self::section_name(state)
+                                                                                                                          }).and_then(|state|
+                                                                                                                                          {
+                                                                                                                                              self::right_bracket(state)
+                                                                                                                                          }).and_then(|state|
+                                                                                                                                                          {
+                                                                                                                                                              state.repeat(|state|
+                                                                                                                                                                               {
+                                                                                                                                                                                   self::space(state)
+                                                                                                                                                                               })
+                                                                                                                                                          })
+                                                                               })
+                                                        })
+                                     })
+                }
+                #[inline]
+                #[allow(non_snake_case, unused_variables)]
+                pub fn comment_line(state: Box<::pest::ParserState<Rule>>)
+                 -> ::pest::ParseResult<Box<::pest::ParserState<Rule>>> {
+                    state.rule(Rule::comment_line,
+                               |state|
+                                   {
+                                       state.atomic(::pest::Atomicity::Atomic,
+                                                    |state|
+                                                        {
+                                                            state.sequence(|state|
+                                                                               {
+                                                                                   self::comment_start(state).and_then(|state|
+                                                                                                                           {
+                                                                                                                               self::line(state)
+                                                                                                                           })
+                                                                               })
+                                                        })
+                                   })
+                }
+                #[inline]
+                #[allow(non_snake_case, unused_variables)]
+                pub fn blank_line(state: Box<::pest::ParserState<Rule>>)
+                 -> ::pest::ParseResult<Box<::pest::ParserState<Rule>>> {
+                    state.rule(Rule::blank_line,
+                               |state|
+                                   {
+                                       state.atomic(::pest::Atomicity::Atomic,
+                                                    |state|
+                                                        {
+                                                            state.repeat(|state|
+                                                                             {
+                                                                                 self::space(state)
+                                                                             })
+                                                        })
+                                   })
+                }
+                #[inline]
+                #[allow(non_snake_case, unused_variables)]
+                pub fn directive(state: Box<::pest::ParserState<Rule>>)
+                 -> ::pest::ParseResult<Box<::pest::ParserState<Rule>>> {
+                    state.atomic(::pest::Atomicity::CompoundAtomic,
+                                 |state|
+                                     {
+                                         state.rule(Rule::directive,
+                                                    |state|
+                                                        {
+                                                            state.sequence(|state|
+                                                                               {
+                                                                                   state.match_string("%").and_then(|state|
+                                                                                                                        {
+                                                                                                                            self::include(state).or_else(|state|
+                                                                                                                                                             {
+                                                                                                                                                                 self::unset(state)
+                                                                                                                                                             })
+                                                                                                                        })
+                                                                               })
+                                                        })
+                                     })
+                }
+                #[inline]
+                #[allow(non_snake_case, unused_variables)]
+                pub fn include(state: Box<::pest::ParserState<Rule>>)
+                 -> ::pest::ParseResult<Box<::pest::ParserState<Rule>>> {
+                    state.atomic(::pest::Atomicity::CompoundAtomic,
+                                 |state|
+                                     {
+                                         state.rule(Rule::include,
+                                                    |state|
+                                                        {
+                                                            state.sequence(|state|
+                                                                               {
+                                                                                   state.match_string("include").and_then(|state|
+                                                                                                                              {
+                                                                                                                                  state.sequence(|state|
+                                                                                                                                                     {
+                                                                                                                                                         self::space(state).and_then(|state|
+                                                                                                                                                                                         {
+                                                                                                                                                                                             state.repeat(|state|
+                                                                                                                                                                                                              {
+                                                                                                                                                                                                                  self::space(state)
+                                                                                                                                                                                                              })
+                                                                                                                                                                                         })
+                                                                                                                                                     })
+                                                                                                                              }).and_then(|state|
+                                                                                                                                              {
+                                                                                                                                                  self::line(state)
+                                                                                                                                              })
+                                                                               })
+                                                        })
+                                     })
+                }
+                #[inline]
+                #[allow(non_snake_case, unused_variables)]
+                pub fn unset(state: Box<::pest::ParserState<Rule>>)
+                 -> ::pest::ParseResult<Box<::pest::ParserState<Rule>>> {
+                    state.atomic(::pest::Atomicity::CompoundAtomic,
+                                 |state|
+                                     {
+                                         state.rule(Rule::unset,
+                                                    |state|
+                                                        {
+                                                            state.sequence(|state|
+                                                                               {
+                                                                                   state.match_string("unset").and_then(|state|
+                                                                                                                            {
+                                                                                                                                state.sequence(|state|
+                                                                                                                                                   {
+                                                                                                                                                       self::space(state).and_then(|state|
+                                                                                                                                                                                       {
+                                                                                                                                                                                           state.repeat(|state|
+                                                                                                                                                                                                            {
+                                                                                                                                                                                                                self::space(state)
+                                                                                                                                                                                                            })
+                                                                                                                                                                                       })
+                                                                                                                                                   })
+                                                                                                                            }).and_then(|state|
+                                                                                                                                            {
+                                                                                                                                                self::config_name(state)
+                                                                                                                                            }).and_then(|state|
+                                                                                                                                                            {
+                                                                                                                                                                state.repeat(|state|
+                                                                                                                                                                                 {
+                                                                                                                                                                                     self::space(state)
+                                                                                                                                                                                 })
+                                                                                                                                                            })
+                                                                               })
+                                                        })
+                                     })
+                }
+                #[inline]
+                #[allow(non_snake_case, unused_variables)]
+                pub fn compound(state: Box<::pest::ParserState<Rule>>)
+                 -> ::pest::ParseResult<Box<::pest::ParserState<Rule>>> {
+                    self::config_item(state).or_else(|state|
+                                                         {
+                                                             self::section(state)
+                                                         }).or_else(|state|
+                                                                        {
+                                                                            self::comment_line(state)
+                                                                        }).or_else(|state|
+                                                                                       {
+                                                                                           self::directive(state)
+                                                                                       }).or_else(|state|
+                                                                                                      {
+                                                                                                          self::blank_line(state)
+                                                                                                      })
+                }
+                #[inline]
+                #[allow(non_snake_case, unused_variables)]
+                pub fn file(state: Box<::pest::ParserState<Rule>>)
+                 -> ::pest::ParseResult<Box<::pest::ParserState<Rule>>> {
+                    state.sequence(|state|
+                                       {
+                                           self::SOI(state).and_then(|state|
+                                                                         {
+                                                                             super::hidden::skip(state)
+                                                                         }).and_then(|state|
+                                                                                         {
+                                                                                             self::compound(state)
+                                                                                         }).and_then(|state|
+                                                                                                         {
+                                                                                                             super::hidden::skip(state)
+                                                                                                         }).and_then(|state|
+                                                                                                                         {
+                                                                                                                             state.sequence(|state|
+                                                                                                                                                {
+                                                                                                                                                    state.optional(|state|
+                                                                                                                                                                       {
+                                                                                                                                                                           state.sequence(|state|
+                                                                                                                                                                                              {
+                                                                                                                                                                                                  self::new_line(state).and_then(|state|
+                                                                                                                                                                                                                                     {
+                                                                                                                                                                                                                                         super::hidden::skip(state)
+                                                                                                                                                                                                                                     }).and_then(|state|
+                                                                                                                                                                                                                                                     {
+                                                                                                                                                                                                                                                         self::compound(state)
+                                                                                                                                                                                                                                                     })
+                                                                                                                                                                                              }).and_then(|state|
+                                                                                                                                                                                                              {
+                                                                                                                                                                                                                  state.repeat(|state|
+                                                                                                                                                                                                                                   {
+                                                                                                                                                                                                                                       state.sequence(|state|
+                                                                                                                                                                                                                                                          {
+                                                                                                                                                                                                                                                              super::hidden::skip(state).and_then(|state|
+                                                                                                                                                                                                                                                                                                      {
+                                                                                                                                                                                                                                                                                                          state.sequence(|state|
+                                                                                                                                                                                                                                                                                                                             {
+                                                                                                                                                                                                                                                                                                                                 self::new_line(state).and_then(|state|
+                                                                                                                                                                                                                                                                                                                                                                    {
+                                                                                                                                                                                                                                                                                                                                                                        super::hidden::skip(state)
+                                                                                                                                                                                                                                                                                                                                                                    }).and_then(|state|
+                                                                                                                                                                                                                                                                                                                                                                                    {
+                                                                                                                                                                                                                                                                                                                                                                                        self::compound(state)
+                                                                                                                                                                                                                                                                                                                                                                                    })
+                                                                                                                                                                                                                                                                                                                             })
+                                                                                                                                                                                                                                                                                                      })
+                                                                                                                                                                                                                                                          })
+                                                                                                                                                                                                                                   })
+                                                                                                                                                                                                              })
+                                                                                                                                                                       })
+                                                                                                                                                })
+                                                                                                                         }).and_then(|state|
+                                                                                                                                         {
+                                                                                                                                             super::hidden::skip(state)
+                                                                                                                                         }).and_then(|state|
+                                                                                                                                                         {
+                                                                                                                                                             self::EOI(state)
+                                                                                                                                                         })
+                                       })
+                }
+                #[inline]
+                #[allow(dead_code, non_snake_case, unused_variables)]
+                pub fn SOI(state: Box<::pest::ParserState<Rule>>)
+                 -> ::pest::ParseResult<Box<::pest::ParserState<Rule>>> {
+                    state.start_of_input()
+                }
+                #[inline]
+                #[allow(dead_code, non_snake_case, unused_variables)]
+                pub fn EOI(state: Box<::pest::ParserState<Rule>>)
+                 -> ::pest::ParseResult<Box<::pest::ParserState<Rule>>> {
+                    state.rule(Rule::EOI, |state| state.end_of_input())
+                }
+                #[inline]
+                #[allow(dead_code, non_snake_case, unused_variables)]
+                pub fn ANY(state: Box<::pest::ParserState<Rule>>)
+                 -> ::pest::ParseResult<Box<::pest::ParserState<Rule>>> {
+                    state.skip(1)
+                }
+            }
+            pub use self::visible::*;
+        }
+        ::pest::state(input,
+                      |state|
+                          {
+                              match rule {
+                                  Rule::new_line => rules::new_line(state),
+                                  Rule::space => rules::space(state),
+                                  Rule::comment_start =>
+                                  rules::comment_start(state),
+                                  Rule::line => rules::line(state),
+                                  Rule::value => rules::value(state),
+                                  Rule::equal_sign =>
+                                  rules::equal_sign(state),
+                                  Rule::config_name =>
+                                  rules::config_name(state),
+                                  Rule::config_item =>
+                                  rules::config_item(state),
+                                  Rule::left_bracket =>
+                                  rules::left_bracket(state),
+                                  Rule::right_bracket =>
+                                  rules::right_bracket(state),
+                                  Rule::section_name =>
+                                  rules::section_name(state),
+                                  Rule::section => rules::section(state),
+                                  Rule::comment_line =>
+                                  rules::comment_line(state),
+                                  Rule::blank_line =>
+                                  rules::blank_line(state),
+                                  Rule::directive => rules::directive(state),
+                                  Rule::include => rules::include(state),
+                                  Rule::unset => rules::unset(state),
+                                  Rule::compound => rules::compound(state),
+                                  Rule::file => rules::file(state),
+                                  Rule::EOI => rules::EOI(state),
+                              }
+                          })
+    }
+}
diff --git a/rust/hg-core/src/configparser/lib.rs b/rust/hg-core/src/configparser/lib.rs
new file mode 100644
--- /dev/null
+++ b/rust/hg-core/src/configparser/lib.rs
@@ -0,0 +1,75 @@
+/*
+ * Copyright (c) Facebook, Inc. and its affiliates.
+ *
+ * This software may be used and distributed according to the terms of the
+ * GNU General Public License version 2.
+ */
+
+//! # ConfigParser
+//!
+//! ConfigParser is a utility to parse hgrc-like config files.
+//!
+//! ## Features
+//!
+//! - Parse valid hgrc-like config files efficiently.
+//! - Track source locations of config values. Keep multiple locations of
+//!   a same config if it is overridden.
+//!
+//! ## Config Format
+//!
+//! hgrc files are similar to INI files:
+//!
+//! ```plain,ignore
+//! [section1]
+//! name1 = value1
+//! name2 = value2
+//!
+//! [section2]
+//! name3 = value3
+//!
+//! ; This is a comment.
+//! # This is also a comment.
+//! ```
+//!
+//! But with some additional features.
+//!
+//! ### Include other config files
+//!
+//! Use `%include` to include other config files:
+//!
+//! ```plain,ignore
+//! %include path/to/another/hgrc
+//! %include path/to/another/hgrc.d
+//! ```
+//!
+//! The include path is relative to the directory of the current config
+//! file being parsed. If it's a directory, files with names ending
+//! with `.rc` in it will be read.
+//!
+//! ### Unset a config
+//!
+//! Use `%unset` to unset a config:
+//!
+//! ```plain,ignore
+//! [section]
+//! %unset name1
+//! ```
+//!
+//! ### Multi-line values
+//!
+//! Indent non-first lines with a space:
+//!
+//! ```plain,ignore
+//! [section]
+//! name1 = value
+//!  line2
+//!  line3
+//! ```
+
+pub mod c_api;
+pub mod config;
+pub mod error;
+pub mod hg;
+pub mod parser;
+
+pub use error::Error;
diff --git a/rust/hg-core/src/configparser/hg.rs b/rust/hg-core/src/configparser/hg.rs
new file mode 100644
--- /dev/null
+++ b/rust/hg-core/src/configparser/hg.rs
@@ -0,0 +1,1054 @@
+/*
+ * Copyright (c) Facebook, Inc. and its affiliates.
+ *
+ * This software may be used and distributed according to the terms of the
+ * GNU General Public License version 2.
+ */
+
+//! Mercurial-specific config postprocessing
+
+use std::cmp::Eq;
+use std::collections::{HashMap, HashSet};
+use std::env;
+use std::hash::Hash;
+use std::path::{Path, PathBuf};
+
+use anyhow::Result;
+use bytes::Bytes;
+use util::path::expand_path;
+
+use crate::config::{ConfigSet, Options};
+use crate::error::Error;
+
+pub const HGPLAIN: &str = "HGPLAIN";
+pub const HGPLAINEXCEPT: &str = "HGPLAINEXCEPT";
+pub const HGRCPATH: &str = "HGRCPATH";
+
+pub trait OptionsHgExt {
+    /// Drop configs according to `$HGPLAIN` and `$HGPLAINEXCEPT`.
+    fn process_hgplain(self) -> Self;
+
+    /// Set read-only config items. `items` contains a list of tuple `(section, name)`.
+    /// Setting those items to new value will be ignored.
+    fn readonly_items<S: Into<Bytes>, N: Into<Bytes>>(self, items: Vec<(S, N)>) -> Self;
+
+    /// Set section remap. If a section name matches an entry key, it will be treated as if the
+    /// name is the entry value. The remap wouldn't happen recursively. For example, with a
+    /// `{"A": "B", "B": "C"}` map, section name "A" will be treated as "B", not "C".
+    /// This is implemented via `append_filter`.
+    fn remap_sections<K: Eq + Hash + Into<Bytes>, V: Into<Bytes>>(
+        self,
+        remap: HashMap<K, V>,
+    ) -> Self;
+
+    /// Set section whitelist. Sections outside the whitelist won't be loaded.
+    /// This is implemented via `append_filter`.
+    fn whitelist_sections<B: Clone + Into<Bytes>>(self, sections: Vec<B>) -> Self;
+}
+
+pub trait ConfigSetHgExt {
+    /// Load system config files if `$HGRCPATH` is not set.
+    /// Return errors parsing files.
+    fn load_system(&mut self) -> Vec<Error>;
+
+    /// Load user config files (and environment variables).  If `$HGRCPATH` is
+    /// set, load files listed in that environment variable instead.
+    /// Return errors parsing files.
+    fn load_user(&mut self) -> Vec<Error>;
+
+    /// Load a specified config file. Respect HGPLAIN environment variables.
+    /// Return errors parsing files.
+    fn load_hgrc(&mut self, path: impl AsRef<Path>, source: &'static str) -> Vec<Error>;
+
+    /// Get a config item. Convert to type `T`.
+    fn get_opt<T: FromConfigValue>(&self, section: &str, name: &str) -> Result<Option<T>>;
+
+    /// Get a config item. Convert to type `T`.
+    ///
+    /// If the config item is not set, calculate it using `default_func`.
+    fn get_or<T: FromConfigValue>(
+        &self,
+        section: &str,
+        name: &str,
+        default_func: impl Fn() -> T,
+    ) -> Result<T> {
+        Ok(self.get_opt(section, name)?.unwrap_or_else(default_func))
+    }
+
+    /// Get a config item. Convert to type `T`.
+    ///
+    /// If the config item is not set, return `T::default()`.
+    fn get_or_default<T: Default + FromConfigValue>(&self, section: &str, name: &str) -> Result<T> {
+        self.get_or(section, name, Default::default)
+    }
+}
+
+pub trait FromConfigValue: Sized {
+    fn try_from_bytes(bytes: &[u8]) -> Result<Self>;
+}
+
+/// Load system, user config files.
+pub fn load() -> Result<ConfigSet> {
+    let mut set = ConfigSet::new();
+    if let Some(error) = set.load_system().pop() {
+        return Err(error.into());
+    }
+    if let Some(error) = set.load_user().pop() {
+        return Err(error.into());
+    }
+    Ok(set)
+}
+
+impl OptionsHgExt for Options {
+    fn process_hgplain(self) -> Self {
+        let plain_set = env::var(HGPLAIN).is_ok();
+        let plain_except = env::var(HGPLAINEXCEPT);
+        if plain_set || plain_except.is_ok() {
+            let (section_blacklist, ui_blacklist) = {
+                let plain_exceptions: HashSet<String> = plain_except
+                    .unwrap_or_else(|_| "".to_string())
+                    .split(',')
+                    .map(|s| s.to_string())
+                    .collect();
+
+                // [defaults] and [commands] are always blacklisted.
+                let mut section_blacklist: HashSet<Bytes> =
+                    ["defaults", "commands"].iter().map(|&s| s.into()).collect();
+
+                // [alias], [revsetalias], [templatealias] are blacklisted if they are outside
+                // HGPLAINEXCEPT.
+                for &name in ["alias", "revsetalias", "templatealias"].iter() {
+                    if !plain_exceptions.contains(name) {
+                        section_blacklist.insert(Bytes::from(name));
+                    }
+                }
+
+                // These configs under [ui] are always blacklisted.
+                let mut ui_blacklist: HashSet<Bytes> = [
+                    "debug",
+                    "fallbackencoding",
+                    "quiet",
+                    "slash",
+                    "logtemplate",
+                    "statuscopies",
+                    "style",
+                    "traceback",
+                    "verbose",
+                ]
+                .iter()
+                .map(|&s| s.into())
+                .collect();
+                // exitcodemask is blacklisted if exitcode is outside HGPLAINEXCEPT.
+                if !plain_exceptions.contains("exitcode") {
+                    ui_blacklist.insert("exitcodemask".into());
+                }
+
+                (section_blacklist, ui_blacklist)
+            };
+
+            let filter = move |section: Bytes, name: Bytes, value: Option<Bytes>| {
+                if section_blacklist.contains(&section)
+                    || (section.as_ref() == b"ui" && ui_blacklist.contains(&name))
+                {
+                    None
+                } else {
+                    Some((section, name, value))
+                }
+            };
+
+            self.append_filter(Box::new(filter))
+        } else {
+            self
+        }
+    }
+
+    /// Set section whitelist. Sections outside the whitelist won't be loaded.
+    /// This is implemented via `append_filter`.
+    fn whitelist_sections<B: Clone + Into<Bytes>>(self, sections: Vec<B>) -> Self {
+        let whitelist: HashSet<Bytes> = sections
+            .iter()
+            .cloned()
+            .map(|section| section.into())
+            .collect();
+
+        let filter = move |section: Bytes, name: Bytes, value: Option<Bytes>| {
+            if whitelist.contains(&section) {
+                Some((section, name, value))
+            } else {
+                None
+            }
+        };
+
+        self.append_filter(Box::new(filter))
+    }
+
+    /// Set section remap. If a section name matches an entry key, it will be treated as if the
+    /// name is the entry value. The remap wouldn't happen recursively. For example, with a
+    /// `{"A": "B", "B": "C"}` map, section name "A" will be treated as "B", not "C".
+    /// This is implemented via `append_filter`.
+    fn remap_sections<K, V>(self, remap: HashMap<K, V>) -> Self
+    where
+        K: Eq + Hash + Into<Bytes>,
+        V: Into<Bytes>,
+    {
+        let remap: HashMap<Bytes, Bytes> = remap
+            .into_iter()
+            .map(|(k, v)| (k.into(), v.into()))
+            .collect();
+
+        let filter = move |section: Bytes, name: Bytes, value: Option<Bytes>| {
+            let section = remap.get(&section).cloned().unwrap_or(section);
+            Some((section, name, value))
+        };
+
+        self.append_filter(Box::new(filter))
+    }
+
+    fn readonly_items<S: Into<Bytes>, N: Into<Bytes>>(self, items: Vec<(S, N)>) -> Self {
+        let readonly_items: HashSet<(Bytes, Bytes)> = items
+            .into_iter()
+            .map(|(section, name)| (section.into(), name.into()))
+            .collect();
+
+        let filter = move |section: Bytes, name: Bytes, value: Option<Bytes>| {
+            if readonly_items.contains(&(section.clone(), name.clone())) {
+                None
+            } else {
+                Some((section, name, value))
+            }
+        };
+
+        self.append_filter(Box::new(filter))
+    }
+}
+
+impl ConfigSetHgExt for ConfigSet {
+    fn load_system(&mut self) -> Vec<Error> {
+        let opts = Options::new().source("system").process_hgplain();
+        let mut errors = Vec::new();
+
+        if env::var(HGRCPATH).is_err() {
+            #[cfg(unix)]
+            {
+                errors.append(&mut self.load_path("/etc/mercurial/system.rc", &opts));
+                // TODO(T40519286): Remove this after the tupperware overrides move out of hgrc.d
+                errors.append(
+                    &mut self.load_path("/etc/mercurial/hgrc.d/tupperware_overrides.rc", &opts),
+                );
+                // TODO(quark): Remove this after packages using system.rc are rolled out
+                errors.append(&mut self.load_path("/etc/mercurial/hgrc.d/include.rc", &opts));
+            }
+
+            #[cfg(windows)]
+            {
+                if let Ok(program_data_path) = env::var("PROGRAMDATA") {
+                    use std::path::Path;
+                    let hgrc_dir = Path::new(&program_data_path).join("Facebook\\Mercurial");
+                    errors.append(&mut self.load_path(hgrc_dir.join("system.rc"), &opts));
+                    // TODO(quark): Remove this after packages using system.rc are rolled out
+                    errors.append(&mut self.load_path(hgrc_dir.join("hgrc"), &opts));
+                }
+            }
+        }
+
+        errors
+    }
+
+    fn load_user(&mut self) -> Vec<Error> {
+        let mut errors = Vec::new();
+
+        // Covert "$VISUAL", "$EDITOR" to "ui.editor".
+        //
+        // Unlike Mercurial, don't convert the "$PAGER" environment variable
+        // to "pager.pager" config.
+        //
+        // The environment variable could be from the system profile (ex.
+        // /etc/profile.d/...), or the user shell rc (ex. ~/.bashrc). There is
+        // no clean way to tell which one it is from.  The value might be
+        // tweaked for sysadmin usecases (ex. -n), which are different from
+        // SCM's usecases.
+        for name in ["VISUAL", "EDITOR"].iter() {
+            if let Ok(editor) = env::var(name) {
+                self.set(
+                    "ui",
+                    "editor",
+                    Some(editor.as_bytes()),
+                    &Options::new().source(format!("${}", name)),
+                );
+                break;
+            }
+        }
+
+        // Convert $HGPROF to profiling.type
+        if let Ok(profiling_type) = env::var("HGPROF") {
+            self.set(
+                "profiling",
+                "type",
+                Some(profiling_type.as_bytes()),
+                &"$HGPROF".into(),
+            );
+        }
+
+        let opts = Options::new().source("user").process_hgplain();
+
+        // If $HGRCPATH is set, use it instead.
+        if let Ok(rcpath) = env::var("HGRCPATH") {
+            #[cfg(unix)]
+            let paths = rcpath.split(':');
+            #[cfg(windows)]
+            let paths = rcpath.split(';');
+            for path in paths {
+                errors.append(&mut self.load_path(expand_path(path), &opts));
+            }
+        } else {
+            if let Some(home_dir) = dirs::home_dir() {
+                errors.append(&mut self.load_path(home_dir.join(".hgrc"), &opts));
+
+                #[cfg(windows)]
+                {
+                    errors.append(&mut self.load_path(home_dir.join("mercurial.ini"), &opts));
+                }
+            }
+            if let Some(config_dir) = dirs::config_dir() {
+                errors.append(&mut self.load_path(config_dir.join("hg/hgrc"), &opts));
+            }
+        }
+
+        errors
+    }
+
+    fn load_hgrc(&mut self, path: impl AsRef<Path>, source: &'static str) -> Vec<Error> {
+        let opts = Options::new().source(source).process_hgplain();
+        self.load_path(path, &opts)
+    }
+
+    fn get_opt<T: FromConfigValue>(&self, section: &str, name: &str) -> Result<Option<T>> {
+        ConfigSet::get(self, section, name)
+            .map(|bytes| T::try_from_bytes(&bytes))
+            .transpose()
+    }
+}
+
+impl FromConfigValue for bool {
+    fn try_from_bytes(bytes: &[u8]) -> Result<Self> {
+        let value = std::str::from_utf8(bytes)?.to_lowercase();
+        match value.as_ref() {
+            "1" | "yes" | "true" | "on" | "always" => Ok(true),
+            "0" | "no" | "false" | "off" | "never" => Ok(false),
+            _ => Err(Error::Convert(format!("invalid bool: {}", value)).into()),
+        }
+    }
+}
+
+impl FromConfigValue for i8 {
+    fn try_from_bytes(bytes: &[u8]) -> Result<Self> {
+        let value = std::str::from_utf8(bytes)?.parse()?;
+        Ok(value)
+    }
+}
+
+impl FromConfigValue for i16 {
+    fn try_from_bytes(bytes: &[u8]) -> Result<Self> {
+        let value = std::str::from_utf8(bytes)?.parse()?;
+        Ok(value)
+    }
+}
+
+impl FromConfigValue for i32 {
+    fn try_from_bytes(bytes: &[u8]) -> Result<Self> {
+        let value = std::str::from_utf8(bytes)?.parse()?;
+        Ok(value)
+    }
+}
+
+impl FromConfigValue for i64 {
+    fn try_from_bytes(bytes: &[u8]) -> Result<Self> {
+        let value = std::str::from_utf8(bytes)?.parse()?;
+        Ok(value)
+    }
+}
+
+impl FromConfigValue for isize {
+    fn try_from_bytes(bytes: &[u8]) -> Result<Self> {
+        let value = std::str::from_utf8(bytes)?.parse()?;
+        Ok(value)
+    }
+}
+
+impl FromConfigValue for u8 {
+    fn try_from_bytes(bytes: &[u8]) -> Result<Self> {
+        let value = std::str::from_utf8(bytes)?.parse()?;
+        Ok(value)
+    }
+}
+
+impl FromConfigValue for u16 {
+    fn try_from_bytes(bytes: &[u8]) -> Result<Self> {
+        let value = std::str::from_utf8(bytes)?.parse()?;
+        Ok(value)
+    }
+}
+
+impl FromConfigValue for u32 {
+    fn try_from_bytes(bytes: &[u8]) -> Result<Self> {
+        let value = std::str::from_utf8(bytes)?.parse()?;
+        Ok(value)
+    }
+}
+
+impl FromConfigValue for u64 {
+    fn try_from_bytes(bytes: &[u8]) -> Result<Self> {
+        let value = std::str::from_utf8(bytes)?.parse()?;
+        Ok(value)
+    }
+}
+
+impl FromConfigValue for usize {
+    fn try_from_bytes(bytes: &[u8]) -> Result<Self> {
+        let value = std::str::from_utf8(bytes)?.parse()?;
+        Ok(value)
+    }
+}
+
+impl FromConfigValue for String {
+    fn try_from_bytes(bytes: &[u8]) -> Result<Self> {
+        String::from_utf8(bytes.to_vec())
+            .map_err(|_| Error::Convert(format!("{:?} is not utf8 encoded", bytes)).into())
+    }
+}
+
+/// Byte count specified with a unit. For example: `1.5 MB`.
+#[derive(Copy, Clone, Default)]
+pub struct ByteCount(u64);
+
+impl ByteCount {
+    /// Get the value of bytes. For example, `1K` has a value of `1024`.
+    pub fn value(self) -> u64 {
+        self.0
+    }
+}
+
+impl From<u64> for ByteCount {
+    fn from(value: u64) -> ByteCount {
+        ByteCount(value)
+    }
+}
+
+impl FromConfigValue for ByteCount {
+    fn try_from_bytes(bytes: &[u8]) -> Result<Self> {
+        // This implementation matches mercurial/util.py:sizetoint
+        let sizeunits = [
+            ("kb", 1u64 << 10),
+            ("mb", 1 << 20),
+            ("gb", 1 << 30),
+            ("tb", 1 << 40),
+            ("k", 1 << 10),
+            ("m", 1 << 20),
+            ("g", 1 << 30),
+            ("t", 1 << 40),
+            ("b", 1),
+            ("", 1),
+        ];
+
+        let value = std::str::from_utf8(bytes)?.to_lowercase();
+        for (suffix, unit) in sizeunits.iter() {
+            if value.ends_with(suffix) {
+                let number_str: &str = value[..value.len() - suffix.len()].trim();
+                let number: f64 = number_str.parse()?;
+                if number < 0.0 {
+                    return Err(Error::Convert(format!(
+                        "byte size '{:?}' cannot be negative",
+                        value
+                    ))
+                    .into());
+                }
+                let unit = *unit as f64;
+                return Ok(ByteCount((number * unit) as u64));
+            }
+        }
+
+        Err(Error::Convert(format!("'{:?}' cannot be parsed as a byte size", value)).into())
+    }
+}
+
+impl FromConfigValue for PathBuf {
+    fn try_from_bytes(bytes: &[u8]) -> Result<Self> {
+        let st = std::str::from_utf8(&bytes)?;
+
+        Ok(expand_path(st))
+    }
+}
+
+impl<T: FromConfigValue> FromConfigValue for Vec<T> {
+    fn try_from_bytes(bytes: &[u8]) -> Result<Self> {
+        let items = parse_list(bytes);
+        items.into_iter().map(|s| T::try_from_bytes(&s)).collect()
+    }
+}
+
+impl<T: FromConfigValue> FromConfigValue for Option<T> {
+    fn try_from_bytes(bytes: &[u8]) -> Result<Self> {
+        T::try_from_bytes(&bytes).map(Option::Some)
+    }
+}
+
+/// Parse a configuration value as a list of comma/space separated strings.
+/// It is ported from `mercurial.config.parselist`.
+///
+/// The function never complains about syntax and always returns some result.
+///
+/// Example:
+///
+/// ```
+/// use configparser::hg::parse_list;
+///
+/// assert_eq!(
+///     parse_list(b"this,is \"a small\" ,test"),
+///     vec![b"this".to_vec(), b"is".to_vec(), b"a small".to_vec(), b"test".to_vec()]
+/// );
+/// ```
+pub fn parse_list<B: AsRef<[u8]>>(value: B) -> Vec<Bytes> {
+    let mut value = value.as_ref();
+
+    // ```python
+    // if value is not None and isinstance(value, bytes):
+    //     result = _configlist(value.lstrip(' ,\n'))
+    // ```
+
+    while b" ,\n".iter().any(|b| value.starts_with(&[*b])) {
+        value = &value[1..]
+    }
+
+    parse_list_internal(value)
+        .into_iter()
+        .map(Bytes::from)
+        .collect()
+}
+
+fn parse_list_internal(value: &[u8]) -> Vec<Vec<u8>> {
+    let mut value = value;
+
+    // ```python
+    // def _configlist(s):
+    //     s = s.rstrip(' ,')
+    //     if not s:
+    //         return []
+    //     parser, parts, offset = _parse_plain, [''], 0
+    //     while parser:
+    //         parser, parts, offset = parser(parts, s, offset)
+    //     return parts
+    // ```
+
+    while b" ,\n".iter().any(|b| value.ends_with(&[*b])) {
+        value = &value[..value.len() - 1]
+    }
+
+    if value.is_empty() {
+        return Vec::new();
+    }
+
+    #[derive(Copy, Clone)]
+    enum State {
+        Plain,
+        Quote,
+    };
+
+    let mut offset = 0;
+    let mut parts: Vec<Vec<u8>> = vec![Vec::new()];
+    let mut state = State::Plain;
+
+    loop {
+        match state {
+            // ```python
+            // def _parse_plain(parts, s, offset):
+            //     whitespace = False
+            //     while offset < len(s) and (s[offset:offset + 1].isspace()
+            //                                or s[offset:offset + 1] == ','):
+            //         whitespace = True
+            //         offset += 1
+            //     if offset >= len(s):
+            //         return None, parts, offset
+            //     if whitespace:
+            //         parts.append('')
+            //     if s[offset:offset + 1] == '"' and not parts[-1]:
+            //         return _parse_quote, parts, offset + 1
+            //     elif s[offset:offset + 1] == '"' and parts[-1][-1:] == '\\':
+            //         parts[-1] = parts[-1][:-1] + s[offset:offset + 1]
+            //         return _parse_plain, parts, offset + 1
+            //     parts[-1] += s[offset:offset + 1]
+            //     return _parse_plain, parts, offset + 1
+            // ```
+            State::Plain => {
+                let mut whitespace = false;
+                while offset < value.len() && b" \n\r\t,".contains(&value[offset]) {
+                    whitespace = true;
+                    offset += 1;
+                }
+                if offset >= value.len() {
+                    break;
+                }
+                if whitespace {
+                    parts.push(Vec::new());
+                }
+                if value[offset] == b'"' {
+                    let branch = {
+                        match parts.last() {
+                            None => 1,
+                            Some(last) => {
+                                if last.is_empty() {
+                                    1
+                                } else if last.ends_with(b"\\") {
+                                    2
+                                } else {
+                                    3
+                                }
+                            }
+                        }
+                    }; // manual NLL, to drop reference on "parts".
+                    if branch == 1 {
+                        // last.is_empty()
+                        state = State::Quote;
+                        offset += 1;
+                        continue;
+                    } else if branch == 2 {
+                        // last.ends_with(b"\\")
+                        let last = parts.last_mut().unwrap();
+                        last.pop();
+                        last.push(value[offset]);
+                        offset += 1;
+                        continue;
+                    }
+                }
+                let last = parts.last_mut().unwrap();
+                last.push(value[offset]);
+                offset += 1;
+            }
+
+            // ```python
+            // def _parse_quote(parts, s, offset):
+            //     if offset < len(s) and s[offset:offset + 1] == '"': # ""
+            //         parts.append('')
+            //         offset += 1
+            //         while offset < len(s) and (s[offset:offset + 1].isspace() or
+            //                 s[offset:offset + 1] == ','):
+            //             offset += 1
+            //         return _parse_plain, parts, offset
+            //     while offset < len(s) and s[offset:offset + 1] != '"':
+            //         if (s[offset:offset + 1] == '\\' and offset + 1 < len(s)
+            //                 and s[offset + 1:offset + 2] == '"'):
+            //             offset += 1
+            //             parts[-1] += '"'
+            //         else:
+            //             parts[-1] += s[offset:offset + 1]
+            //         offset += 1
+            //     if offset >= len(s):
+            //         real_parts = _configlist(parts[-1])
+            //         if not real_parts:
+            //             parts[-1] = '"'
+            //         else:
+            //             real_parts[0] = '"' + real_parts[0]
+            //             parts = parts[:-1]
+            //             parts.extend(real_parts)
+            //         return None, parts, offset
+            //     offset += 1
+            //     while offset < len(s) and s[offset:offset + 1] in [' ', ',']:
+            //         offset += 1
+            //     if offset < len(s):
+            //         if offset + 1 == len(s) and s[offset:offset + 1] == '"':
+            //             parts[-1] += '"'
+            //             offset += 1
+            //         else:
+            //             parts.append('')
+            //     else:
+            //         return None, parts, offset
+            //     return _parse_plain, parts, offset
+            // ```
+            State::Quote => {
+                if offset < value.len() && value[offset] == b'"' {
+                    parts.push(Vec::new());
+                    offset += 1;
+                    while offset < value.len() && b" \n\r\t,".contains(&value[offset]) {
+                        offset += 1;
+                    }
+                    state = State::Plain;
+                    continue;
+                }
+                while offset < value.len() && value[offset] != b'"' {
+                    if value[offset] == b'\\'
+                        && offset + 1 < value.len()
+                        && value[offset + 1] == b'"'
+                    {
+                        offset += 1;
+                        parts.last_mut().unwrap().push(b'"');
+                    } else {
+                        parts.last_mut().unwrap().push(value[offset]);
+                    }
+                    offset += 1;
+                }
+                if offset >= value.len() {
+                    let mut real_parts: Vec<Vec<u8>> = parse_list_internal(parts.last().unwrap())
+                        .iter()
+                        .map(|b| b.to_vec())
+                        .collect();
+                    if real_parts.is_empty() {
+                        parts.pop();
+                        parts.push(vec![b'"']);
+                    } else {
+                        real_parts[0].insert(0, b'"');
+                        parts.pop();
+                        parts.append(&mut real_parts);
+                    }
+                    break;
+                }
+                offset += 1;
+                while offset < value.len() && b" ,".contains(&value[offset]) {
+                    offset += 1;
+                }
+                if offset < value.len() {
+                    if offset + 1 == value.len() && value[offset] == b'"' {
+                        parts.last_mut().unwrap().push(b'"');
+                        offset += 1;
+                    } else {
+                        parts.push(Vec::new());
+                    }
+                } else {
+                    break;
+                }
+                state = State::Plain;
+            }
+        }
+    }
+
+    parts
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    use tempdir::TempDir;
+
+    use crate::config::tests::write_file;
+
+    use lazy_static::lazy_static;
+    use parking_lot::Mutex;
+
+    lazy_static! {
+        /// Lock for the environment.  This should be acquired by tests that rely on particular
+        /// environment variable values that might be overwritten by other tests.
+        static ref ENV_LOCK: Mutex<()> = Mutex::new(());
+    }
+
+    #[test]
+    fn test_basic_hgplain() {
+        let _guard = ENV_LOCK.lock();
+        env::set_var(HGPLAIN, "1");
+        env::remove_var(HGPLAINEXCEPT);
+
+        let opts = Options::new().process_hgplain();
+        let mut cfg = ConfigSet::new();
+        cfg.parse(
+            "[defaults]\n\
+             commit = commit -d 0\n\
+             [ui]\n\
+             verbose = true\n\
+             username = test\n\
+             [alias]\n\
+             l = log\n",
+            &opts,
+        );
+
+        assert!(cfg.keys("defaults").is_empty());
+        assert_eq!(cfg.get("ui", "verbose"), None);
+        assert_eq!(cfg.get("ui", "username"), Some("test".into()));
+        assert_eq!(cfg.get("alias", "l"), None);
+    }
+
+    #[test]
+    fn test_hgplainexcept() {
+        let _guard = ENV_LOCK.lock();
+        env::remove_var(HGPLAIN);
+        env::set_var(HGPLAINEXCEPT, "alias,revsetalias");
+
+        let opts = Options::new().process_hgplain();
+        let mut cfg = ConfigSet::new();
+        cfg.parse(
+            "[defaults]\n\
+             commit = commit -d 0\n\
+             [alias]\n\
+             l = log\n\
+             [templatealias]\n\
+             u = user\n\
+             [revsetalias]\n\
+             @ = master\n",
+            &opts,
+        );
+
+        assert!(cfg.keys("defaults").is_empty());
+        assert_eq!(cfg.get("alias", "l"), Some("log".into()));
+        assert_eq!(cfg.get("revsetalias", "@"), Some("master".into()));
+        assert_eq!(cfg.get("templatealias", "u"), None);
+    }
+
+    #[test]
+    fn test_hgrcpath() {
+        let dir = TempDir::new("test_hgrcpath").unwrap();
+
+        write_file(dir.path().join("1.rc"), "[x]\na=1");
+        write_file(dir.path().join("2.rc"), "[y]\nb=2");
+
+        #[cfg(unix)]
+        let hgrcpath = "$T/1.rc:$T/2.rc";
+        #[cfg(windows)]
+        let hgrcpath = "$T/1.rc;%T%/2.rc";
+
+        env::set_var("T", dir.path());
+        env::set_var(HGRCPATH, hgrcpath);
+
+        let mut cfg = ConfigSet::new();
+
+        cfg.load_system();
+        assert!(cfg.sections().is_empty());
+
+        cfg.load_user();
+        assert_eq!(cfg.get("x", "a"), Some("1".into()));
+        assert_eq!(cfg.get("y", "b"), Some("2".into()));
+    }
+
+    #[test]
+    fn test_load_hgrc() {
+        let dir = TempDir::new("test_hgrcpath").unwrap();
+        let path = dir.path().join("1.rc");
+
+        write_file(path.clone(), "[x]\na=1\n[alias]\nb=c\n");
+
+        let _guard = ENV_LOCK.lock();
+        env::set_var(HGPLAIN, "1");
+        env::remove_var(HGPLAINEXCEPT);
+
+        let mut cfg = ConfigSet::new();
+        cfg.load_hgrc(&path, "hgrc");
+
+        assert!(cfg.keys("alias").is_empty());
+        assert!(cfg.get("alias", "b").is_none());
+        assert_eq!(cfg.get("x", "a").unwrap(), "1");
+
+        env::remove_var(HGPLAIN);
+        cfg.load_hgrc(&path, "hgrc");
+
+        assert_eq!(cfg.get("alias", "b").unwrap(), "c");
+    }
+
+    #[test]
+    fn test_section_whitelist() {
+        let opts = Options::new().whitelist_sections(vec!["x", "y"]);
+        let mut cfg = ConfigSet::new();
+        cfg.parse(
+            "[x]\n\
+             a=1\n\
+             [y]\n\
+             b=2\n\
+             [z]\n\
+             c=3",
+            &opts,
+        );
+
+        assert_eq!(cfg.sections(), vec![Bytes::from("x"), Bytes::from("y")]);
+        assert_eq!(cfg.get("z", "c"), None);
+    }
+
+    #[test]
+    fn test_section_remap() {
+        let mut remap = HashMap::new();
+        remap.insert("x", "y");
+        remap.insert("y", "z");
+
+        let opts = Options::new().remap_sections(remap);
+        let mut cfg = ConfigSet::new();
+        cfg.parse(
+            "[x]\n\
+             a=1\n\
+             [y]\n\
+             b=2\n\
+             [z]\n\
+             c=3",
+            &opts,
+        );
+
+        assert_eq!(cfg.get("y", "a"), Some("1".into()));
+        assert_eq!(cfg.get("z", "b"), Some("2".into()));
+        assert_eq!(cfg.get("z", "c"), Some("3".into()));
+    }
+
+    #[test]
+    fn test_readonly_items() {
+        let opts = Options::new().readonly_items(vec![("x", "a"), ("y", "b")]);
+        let mut cfg = ConfigSet::new();
+        cfg.parse(
+            "[x]\n\
+             a=1\n\
+             [y]\n\
+             b=2\n\
+             [z]\n\
+             c=3",
+            &opts,
+        );
+
+        assert_eq!(cfg.get("x", "a"), None);
+        assert_eq!(cfg.get("y", "b"), None);
+        assert_eq!(cfg.get("z", "c"), Some("3".into()));
+    }
+
+    #[test]
+    fn test_parse_list() {
+        fn b<B: AsRef<[u8]>>(bytes: B) -> Bytes {
+            Bytes::from(bytes.as_ref())
+        }
+
+        // From test-ui-config.py
+        assert_eq!(parse_list(b"foo"), vec![b("foo")]);
+        assert_eq!(
+            parse_list(b"foo bar baz"),
+            vec![b("foo"), b("bar"), b("baz")]
+        );
+        assert_eq!(parse_list(b"alice, bob"), vec![b("alice"), b("bob")]);
+        assert_eq!(
+            parse_list(b"foo bar baz alice, bob"),
+            vec![b("foo"), b("bar"), b("baz"), b("alice"), b("bob")]
+        );
+        assert_eq!(
+            parse_list(b"abc d\"ef\"g \"hij def\""),
+            vec![b("abc"), b("d\"ef\"g"), b("hij def")]
+        );
+        assert_eq!(
+            parse_list(b"\"hello world\", \"how are you?\""),
+            vec![b("hello world"), b("how are you?")]
+        );
+        assert_eq!(
+            parse_list(b"Do\"Not\"Separate"),
+            vec![b("Do\"Not\"Separate")]
+        );
+        assert_eq!(parse_list(b"\"Do\"Separate"), vec![b("Do"), b("Separate")]);
+        assert_eq!(
+            parse_list(b"\"Do\\\"NotSeparate\""),
+            vec![b("Do\"NotSeparate")]
+        );
+        assert_eq!(
+            parse_list(&b"string \"with extraneous\" quotation mark\""[..]),
+            vec![
+                b("string"),
+                b("with extraneous"),
+                b("quotation"),
+                b("mark\""),
+            ]
+        );
+        assert_eq!(parse_list(b"x, y"), vec![b("x"), b("y")]);
+        assert_eq!(parse_list(b"\"x\", \"y\""), vec![b("x"), b("y")]);
+        assert_eq!(
+            parse_list(b"\"\"\" key = \"x\", \"y\" \"\"\""),
+            vec![b(""), b(" key = "), b("x\""), b("y"), b(""), b("\"")]
+        );
+        assert_eq!(parse_list(b",,,,     "), Vec::<Bytes>::new());
+        assert_eq!(
+            parse_list(b"\" just with starting quotation"),
+            vec![b("\""), b("just"), b("with"), b("starting"), b("quotation")]
+        );
+        assert_eq!(
+            parse_list(&b"\"longer quotation\" with \"no ending quotation"[..]),
+            vec![
+                b("longer quotation"),
+                b("with"),
+                b("\"no"),
+                b("ending"),
+                b("quotation"),
+            ]
+        );
+        assert_eq!(
+            parse_list(&b"this is \\\" \"not a quotation mark\""[..]),
+            vec![b("this"), b("is"), b("\""), b("not a quotation mark")]
+        );
+        assert_eq!(parse_list(b"\n \n\nding\ndong"), vec![b("ding"), b("dong")]);
+
+        // Other manually written cases
+        assert_eq!(parse_list("a,b,,c"), vec![b("a"), b("b"), b("c")]);
+        assert_eq!(parse_list("a b  c"), vec![b("a"), b("b"), b("c")]);
+        assert_eq!(
+            parse_list(" , a , , b,  , c , "),
+            vec![b("a"), b("b"), b("c")]
+        );
+        assert_eq!(parse_list("a,\"b,c\" d"), vec![b("a"), b("b,c"), b("d")]);
+        assert_eq!(parse_list("a,\",c"), vec![b("a"), b("\""), b("c")]);
+        assert_eq!(parse_list("a,\" c\" \""), vec![b("a"), b(" c\"")]);
+        assert_eq!(
+            parse_list("a,\" c\" \" d"),
+            vec![b("a"), b(" c"), b("\""), b("d")]
+        );
+    }
+
+    #[test]
+    fn test_get_or() {
+        let mut cfg = ConfigSet::new();
+        cfg.parse(
+            "[foo]\n\
+             bool1 = yes\n\
+             bool2 = unknown\n\
+             bools = 1, TRUE, On, aLwAys, 0, false, oFF, never\n\
+             int1 = -33\n\
+             list1 = x y z\n\
+             list3 = 2, 3, 1\n\
+             byte1 = 1.5 KB\n\
+             byte2 = 500\n\
+             byte3 = 0.125M\n\
+             ",
+            &"test".into(),
+        );
+
+        assert_eq!(cfg.get_or("foo", "bar", || 3).unwrap(), 3);
+        assert_eq!(cfg.get_or("foo", "bool1", || false).unwrap(), true);
+        assert_eq!(
+            format!("{}", cfg.get_or("foo", "bool2", || true).unwrap_err()),
+            "invalid bool: unknown"
+        );
+        assert_eq!(cfg.get_or("foo", "int1", || 42).unwrap(), -33);
+        assert_eq!(
+            cfg.get_or("foo", "list1", || vec!["x".to_string()])
+                .unwrap(),
+            vec!["x", "y", "z"]
+        );
+        assert_eq!(
+            cfg.get_or("foo", "list3", || vec![0]).unwrap(),
+            vec![2, 3, 1]
+        );
+
+        assert_eq!(cfg.get_or_default::<bool>("foo", "bool1").unwrap(), true);
+        assert_eq!(
+            cfg.get_or_default::<Vec<bool>>("foo", "bools").unwrap(),
+            vec![true, true, true, true, false, false, false, false]
+        );
+
+        assert_eq!(
+            cfg.get_or_default::<ByteCount>("foo", "byte1")
+                .unwrap()
+                .value(),
+            1536
+        );
+        assert_eq!(
+            cfg.get_or_default::<ByteCount>("foo", "byte2")
+                .unwrap()
+                .value(),
+            500
+        );
+        assert_eq!(
+            cfg.get_or_default::<ByteCount>("foo", "byte3")
+                .unwrap()
+                .value(),
+            131072
+        );
+        assert_eq!(
+            cfg.get_or("foo", "missing", || ByteCount::from(3))
+                .unwrap()
+                .value(),
+            3
+        );
+    }
+}
diff --git a/rust/hg-core/src/configparser/generate_parser.py b/rust/hg-core/src/configparser/generate_parser.py
new file mode 100755
--- /dev/null
+++ b/rust/hg-core/src/configparser/generate_parser.py
@@ -0,0 +1,119 @@
+#!/usr/bin/env python3
+
+# Copyright 2018 Facebook, Inc.
+#
+# This software may be used and distributed according to the terms of the
+# GNU General Public License version 2 or any later version.
+
+
+import hashlib
+import os
+import re
+import subprocess
+import tempfile
+
+
+dirname = os.path.dirname
+
+crate_root = dirname(dirname(os.path.realpath(__file__)))
+
+
+def expand_parser(pest):
+    """expand the "#[derive(Parser)] part"""
+    with tempfile.TemporaryDirectory() as tmp_root:
+        # Copy Cargo.toml, without [dev-dependencies] and [[bench]]
+        with open(os.path.join(tmp_root, "Cargo.toml"), "w") as f:
+            content = open(os.path.join(crate_root, "Cargo.toml")).read()
+            content = content.split("[dev-dependencies]")[0]
+            f.write(content)
+
+        # Copy spec.pest
+        os.mkdir(os.path.join(tmp_root, "src"))
+        with open(os.path.join(tmp_root, "src", "spec.pest"), "wb") as f:
+            f.write(pest)
+
+        # Create a minimal project which is used to expand ConfigParser
+        with open(os.path.join(tmp_root, "src", "lib.rs"), "w") as f:
+            f.write(
+                """
+#[derive(Parser)]
+#[grammar = "spec.pest"]
+pub(crate) struct ConfigParser;
+"""
+            )
+
+        # Run cargo-expand
+        env = os.environ.copy()
+        env["RUSTFMT"] = "false"
+        expanded = subprocess.check_output(
+            ["cargo-expand", "--release"], env=env, cwd=tmp_root
+        )
+        expanded = expanded.decode("utf-8")
+
+        # Keep only interesting parts
+        rule_struct = re.search(
+            "^pub enum Rule [^}]*^\}", expanded, re.S + re.M
+        ).group(0)
+        parser_impl = re.search(
+            "^impl ::pest::Parser<Rule> for ConfigParser .*^\}",
+            expanded,
+            re.S + re.M,
+        ).group(0)
+
+        code = f"""
+#[allow(dead_code, non_camel_case_types)]
+#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
+{rule_struct}
+
+pub(crate) struct ConfigParser;
+
+{parser_impl}
+"""
+
+        return code
+
+
+def write_generated_parser():
+    spec_pest_path = os.path.join(crate_root, "src", "spec.pest")
+    spec = open(spec_pest_path, "rb").read()
+
+    checksum = hashlib.sha1(spec).hexdigest()
+    output_path = os.path.join(crate_root, "src", "parser.rs")
+
+    try:
+        old_checksum = re.search(
+            "pest-checksum: (.*)\.", open(output_path).read()
+        ).group(1)
+        if old_checksum == checksum:
+            print(
+                "No need to update %s because %s is not changed."
+                % (output_path, spec_pest_path)
+            )
+            return
+    except Exception:
+        pass
+
+    with open(output_path, "w") as f:
+        code = expand_parser(spec)
+        f.write(
+            f"""
+// Generated by generate_parser.py. Do not edit manually. Instead, edit
+// spec.pest, then run generate_parser.py (require cargo-expand).
+//
+// This file should really be just 3 lines:
+//
+// #[derive(Parser)]
+// #[grammar = "spec.pest"]
+// pub(crate) struct ConfigParser;
+//
+// However, `#[grammar = "spec.pest"]` does not play well with Buck build,
+// because pest_derive cannot find "spec.pest" in buck build environment.
+// Therefore this file is {'detareneg@'[::-1]}. {"tnil-on@"[::-1]}.
+// pest-checksum: {checksum}.
+
+{code}"""
+        )
+
+
+if __name__ == "__main__":
+    write_generated_parser()
diff --git a/rust/hg-core/src/configparser/error.rs b/rust/hg-core/src/configparser/error.rs
new file mode 100644
--- /dev/null
+++ b/rust/hg-core/src/configparser/error.rs
@@ -0,0 +1,32 @@
+/*
+ * Copyright (c) Facebook, Inc. and its affiliates.
+ *
+ * This software may be used and distributed according to the terms of the
+ * GNU General Public License version 2.
+ */
+
+use std::io;
+use std::path::PathBuf;
+use std::str;
+
+use thiserror::Error;
+
+/// The error type for parsing config files.
+#[derive(Error, Debug)]
+pub enum Error {
+    /// Unable to convert to a type.
+    #[error("{0}")]
+    Convert(String),
+
+    /// Unable to parse a file due to syntax.
+    #[error("{0:?}:\n{1}")]
+    Parse(PathBuf, String),
+
+    /// Unable to read a file due to IO errors.
+    #[error("{0:?}: {1}")]
+    Io(PathBuf, #[source] io::Error),
+
+    /// Config file contains invalid UTF-8.
+    #[error("{0:?}: {1}")]
+    Utf8(PathBuf, #[source] str::Utf8Error),
+}
diff --git a/rust/hg-core/src/configparser/config.rs b/rust/hg-core/src/configparser/config.rs
new file mode 100644
--- /dev/null
+++ b/rust/hg-core/src/configparser/config.rs
@@ -0,0 +1,944 @@
+/*
+ * Copyright (c) Facebook, Inc. and its affiliates.
+ *
+ * This software may be used and distributed according to the terms of the
+ * GNU General Public License version 2.
+ */
+
+use std::collections::HashSet;
+use std::convert::AsRef;
+use std::fs;
+use std::io::Read;
+use std::ops::Range;
+use std::path::{Path, PathBuf};
+use std::str;
+use std::sync::Arc;
+
+use bytes::Bytes;
+use indexmap::IndexMap;
+use pest::{self, Parser, Span};
+use util::path::expand_path;
+
+use crate::error::Error;
+use crate::parser::{ConfigParser, Rule};
+
+type Pair<'a> = pest::iterators::Pair<'a, Rule>;
+
+/// Collection of config sections loaded from various sources.
+#[derive(Clone, Default, Debug)]
+pub struct ConfigSet {
+    sections: IndexMap<Bytes, Section>,
+}
+
+/// Internal representation of a config section.
+#[derive(Clone, Default, Debug)]
+struct Section {
+    items: IndexMap<Bytes, Vec<ValueSource>>,
+}
+
+/// A config value with associated metadata like where it comes from.
+#[derive(Clone, Debug)]
+pub struct ValueSource {
+    value: Option<Bytes>,
+    source: Bytes, // global, user, repo, "--config", or an extension name, etc.
+    location: Option<ValueLocation>,
+}
+
+/// The on-disk file name and byte offsets that provide the config value.
+/// Useful if applications want to edit config values in-place.
+#[derive(Clone, Debug)]
+struct ValueLocation {
+    path: Arc<PathBuf>,
+    content: Bytes,
+    location: Range<usize>,
+}
+
+/// Options that affects config setting functions like `load_path`, `parse`,
+/// and `set`.
+#[derive(Default)]
+pub struct Options {
+    source: Bytes,
+    filters: Vec<Box<dyn Fn(Bytes, Bytes, Option<Bytes>) -> Option<(Bytes, Bytes, Option<Bytes>)>>>,
+}
+
+impl ConfigSet {
+    /// Return an empty `ConfigSet`.
+    pub fn new() -> Self {
+        Default::default()
+    }
+
+    /// Load config files at given path. The path is a file.
+    ///
+    /// If `path` is a directory, it is ignored.
+    /// If `path` is a file, it will be loaded directly.
+    ///
+    /// A config file can use `%include` to load other paths (directories or files). They will
+    /// be loaded recursively. Includes take effect in place, instead of deferred. For example,
+    /// with the following two files:
+    ///
+    /// ```plain,ignore
+    /// # This is 1.rc
+    /// [section]
+    /// x = 1
+    /// %include 2.rc
+    /// y = 2
+    ///
+    /// # This is 2.rc
+    /// [section]
+    /// x = 3
+    /// y = 4
+    /// ```
+    ///
+    /// After loading `1.rc`. `x` is set to 3 and `y` is set to 2.
+    ///
+    /// Loading a file that is already parsed or being parsed by this `load_path` call is ignored,
+    /// to avoid infinite loop. A separate `load_path` call would not ignore files loaded by
+    /// other `load_path` calls.
+    ///
+    /// Return a list of errors. An error pasing a file will stop that file from loading, without
+    /// affecting other files.
+    pub fn load_path<P: AsRef<Path>>(&mut self, path: P, opts: &Options) -> Vec<Error> {
+        let mut visited = HashSet::new();
+        let mut errors = Vec::new();
+        self.load_file(path.as_ref(), opts, &mut visited, &mut errors);
+        errors
+    }
+
+    /// Load content of an unnamed config file. The `ValueLocation`s of loaded config items will
+    /// have an empty `path`.
+    ///
+    /// Return a list of errors.
+    pub fn parse<B: Into<Bytes>>(&mut self, content: B, opts: &Options) -> Vec<Error> {
+        let mut visited = HashSet::new();
+        let mut errors = Vec::new();
+        let buf = content.into();
+        self.load_file_content(Path::new(""), buf, opts, &mut visited, &mut errors);
+        errors
+    }
+
+    /// Get config sections.
+    pub fn sections(&self) -> Vec<Bytes> {
+        self.sections.keys().cloned().collect()
+    }
+
+    /// Get config names in the given section. Sorted by insertion order.
+    pub fn keys<S: Into<Bytes>>(&self, section: S) -> Vec<Bytes> {
+        self.sections
+            .get(&section.into())
+            .map(|section| section.items.keys().cloned().collect())
+            .unwrap_or(Vec::new())
+    }
+
+    /// Get config value for a given config.
+    /// Return `None` if the config item does not exist or is unset.
+    pub fn get<S: Into<Bytes>, N: Into<Bytes>>(&self, section: S, name: N) -> Option<Bytes> {
+        self.sections.get(&section.into()).and_then(|section| {
+            section
+                .items
+                .get(&name.into())
+                .and_then(|values| values.last().and_then(|value| value.value.clone()))
+        })
+    }
+
+    /// Get detailed sources of a given config, including overrides, and source information.
+    /// The last item in the returned vector is the latest value that is considered effective.
+    ///
+    /// Return an emtpy vector if the config does not exist.
+    pub fn get_sources<S: Into<Bytes>, N: Into<Bytes>>(
+        &self,
+        section: S,
+        name: N,
+    ) -> Vec<ValueSource> {
+        self.sections
+            .get(&section.into())
+            .and_then(|section| section.items.get(&name.into()).map(|values| values.clone()))
+            .unwrap_or(Vec::new())
+    }
+
+    /// Set a config item directly. `section`, `name` locates the config. `value` is the new value.
+    /// `source` is some annotation about who set it, ex. "reporc", "userrc", "--config", etc.
+    pub fn set<T: Into<Bytes>, N: Into<Bytes>>(
+        &mut self,
+        section: T,
+        name: N,
+        value: Option<&[u8]>,
+        opts: &Options,
+    ) {
+        let section = section.into();
+        let name = name.into();
+        let value = value.map(|v| Bytes::from(v));
+        self.set_internal(section, name, value, None, &opts)
+    }
+
+    fn set_internal(
+        &mut self,
+        section: Bytes,
+        name: Bytes,
+        value: Option<Bytes>,
+        location: Option<ValueLocation>,
+        opts: &Options,
+    ) {
+        let filtered = opts
+            .filters
+            .iter()
+            .fold(Some((section, name, value)), move |acc, func| {
+                acc.and_then(|(section, name, value)| func(section, name, value))
+            });
+        if let Some((section, name, value)) = filtered {
+            self.sections
+                .entry(section)
+                .or_insert_with(|| Default::default())
+                .items
+                .entry(name)
+                .or_insert_with(|| Vec::with_capacity(1))
+                .push(ValueSource {
+                    value,
+                    location,
+                    source: opts.source.clone(),
+                })
+        }
+    }
+
+    fn load_file(
+        &mut self,
+        path: &Path,
+        opts: &Options,
+        visited: &mut HashSet<PathBuf>,
+        errors: &mut Vec<Error>,
+    ) {
+        if let Ok(path) = path.canonicalize() {
+            let path = &path;
+            debug_assert!(path.is_absolute());
+
+            if !visited.insert(path.to_path_buf()) {
+                // skip - visited before
+                return;
+            }
+
+            match fs::File::open(path) {
+                Ok(mut file) => {
+                    let mut buf = Vec::with_capacity(256);
+                    if let Err(error) = file.read_to_end(&mut buf) {
+                        errors.push(Error::Io(path.to_path_buf(), error));
+                        return;
+                    }
+                    buf.push(b'\n');
+                    let buf = Bytes::from(buf);
+
+                    self.load_file_content(path, buf, opts, visited, errors);
+                }
+                Err(error) => errors.push(Error::Io(path.to_path_buf(), error)),
+            }
+        } else {
+            // On Windows, a UNC path `\\?\C:\foo\.\x` will fail to canonicalize
+            // because it contains `.`. That path can be constructed by using
+            // `PathBuf::join` to concatenate a UNC path `\\?\C:\foo` with
+            // a "normal" path `.\x`.
+            // Try to fix it automatically by stripping the UNC prefix and retry
+            // `canonicalize`. `C:\foo\.\x` would be canonicalized without errors.
+            #[cfg(windows)]
+            {
+                if let Some(path_str) = path.to_str() {
+                    if path_str.starts_with(r"\\?\") {
+                        let path = Path::new(&path_str[4..]);
+                        self.load_file(&path, opts, visited, errors);
+                    }
+                }
+            }
+        }
+
+        // If `path.canonicalize` reports an error. It's usually the path cannot
+        // be resolved (ex. does not exist). It is considered normal and is not
+        // reported in `errors`.
+    }
+
+    fn load_file_content(
+        &mut self,
+        path: &Path,
+        buf: Bytes,
+        opts: &Options,
+        visited: &mut HashSet<PathBuf>,
+        errors: &mut Vec<Error>,
+    ) {
+        let mut section = Bytes::new();
+        let shared_path = Arc::new(path.to_path_buf()); // use Arc to do shallow copy
+        let skip_include = path.parent().is_none(); // skip handling %include if path is empty
+
+        // Utilities to avoid too much indentation.
+        let handle_value = |this: &mut ConfigSet,
+                            pair: Pair,
+                            section: Bytes,
+                            name: Bytes,
+                            location: ValueLocation| {
+            let pairs = pair.into_inner();
+            let mut lines = Vec::with_capacity(1);
+            for pair in pairs {
+                if Rule::line == pair.as_rule() {
+                    lines.push(extract(&buf, pair.as_span()));
+                }
+            }
+
+            let value = match lines.len() {
+                1 => lines[0].clone(),
+                _ => Bytes::from(lines.join(&b'\n')),
+            };
+
+            let (start, end) = strip_offsets(&value, 0, value.len());
+            let value = value.slice(start, end);
+
+            this.set_internal(section, name, value.into(), location.into(), opts)
+        };
+
+        let handle_config_item = |this: &mut ConfigSet, pair: Pair, section: Bytes| {
+            let pairs = pair.into_inner();
+            let mut name = Bytes::new();
+            for pair in pairs {
+                match pair.as_rule() {
+                    Rule::config_name => name = extract(&buf, pair.as_span()),
+                    Rule::value => {
+                        let span = pair.as_span();
+                        let location = ValueLocation {
+                            path: shared_path.clone(),
+                            content: buf.clone(),
+                            location: span.start()..span.end(),
+                        };
+                        return handle_value(this, pair, section, name, location);
+                    }
+                    _ => (),
+                }
+            }
+            unreachable!();
+        };
+
+        let handle_section = |pair: Pair, section: &mut Bytes| {
+            let pairs = pair.into_inner();
+            for pair in pairs {
+                match pair.as_rule() {
+                    Rule::section_name => {
+                        *section = extract(&buf, pair.as_span());
+                        return;
+                    }
+                    _ => (),
+                }
+            }
+            unreachable!();
+        };
+
+        let mut handle_include = |this: &mut ConfigSet, pair: Pair, errors: &mut Vec<Error>| {
+            let pairs = pair.into_inner();
+            for pair in pairs {
+                match pair.as_rule() {
+                    Rule::line => {
+                        if !skip_include {
+                            let include_path = pair.as_str();
+                            let full_include_path =
+                                path.parent().unwrap().join(expand_path(include_path));
+                            this.load_file(&full_include_path, opts, visited, errors);
+                        }
+                    }
+                    _ => (),
+                }
+            }
+        };
+
+        let handle_unset = |this: &mut ConfigSet, pair: Pair, section: &Bytes| {
+            let unset_span = pair.as_span();
+            let pairs = pair.into_inner();
+            for pair in pairs {
+                match pair.as_rule() {
+                    Rule::config_name => {
+                        let name = extract(&buf, pair.as_span());
+                        let location = ValueLocation {
+                            path: shared_path.clone(),
+                            content: buf.clone(),
+                            location: unset_span.start()..unset_span.end(),
+                        };
+                        return this.set_internal(
+                            section.clone(),
+                            name,
+                            None,
+                            location.into(),
+                            opts,
+                        );
+                    }
+                    _ => (),
+                }
+            }
+            unreachable!();
+        };
+
+        let mut handle_directive =
+            |this: &mut ConfigSet, pair: Pair, section: &Bytes, errors: &mut Vec<Error>| {
+                let pairs = pair.into_inner();
+                for pair in pairs {
+                    match pair.as_rule() {
+                        Rule::include => handle_include(this, pair, errors),
+                        Rule::unset => handle_unset(this, pair, section),
+                        _ => (),
+                    }
+                }
+            };
+
+        let text = match str::from_utf8(&buf) {
+            Ok(text) => text,
+            Err(error) => return errors.push(Error::Utf8(path.to_path_buf(), error)),
+        };
+
+        let pairs = match ConfigParser::parse(Rule::file, &text) {
+            Ok(pairs) => pairs,
+            Err(error) => {
+                return errors.push(Error::Parse(path.to_path_buf(), format!("{}", error)));
+            }
+        };
+
+        for pair in pairs {
+            match pair.as_rule() {
+                Rule::config_item => handle_config_item(self, pair, section.clone()),
+                Rule::section => handle_section(pair, &mut section),
+                Rule::directive => handle_directive(self, pair, &section, errors),
+                Rule::blank_line | Rule::comment_line | Rule::new_line | Rule::EOI => (),
+
+                Rule::comment_start
+                | Rule::compound
+                | Rule::config_name
+                | Rule::equal_sign
+                | Rule::file
+                | Rule::include
+                | Rule::left_bracket
+                | Rule::line
+                | Rule::right_bracket
+                | Rule::section_name
+                | Rule::space
+                | Rule::unset
+                | Rule::value => unreachable!(),
+            }
+        }
+    }
+}
+
+impl ValueSource {
+    /// Return the actual value stored in this config value, or `None` if uset.
+    pub fn value(&self) -> &Option<Bytes> {
+        &self.value
+    }
+
+    /// Return the "source" information for the config value. It's usually who sets the config,
+    /// like "--config", "user_hgrc", "system_hgrc", etc.
+    pub fn source(&self) -> &Bytes {
+        &self.source
+    }
+
+    /// Return the file path and byte range for the exact config value,
+    /// or `None` if there is no such information.
+    ///
+    /// If the value is `None`, the byte range is for the "%unset" statement.
+    pub fn location(&self) -> Option<(PathBuf, Range<usize>)> {
+        match self.location {
+            Some(ref src) => Some((src.path.as_ref().to_path_buf(), src.location.clone())),
+            None => None,
+        }
+    }
+
+    /// Return the file content. Or `None` if there is no such information.
+    pub fn file_content(&self) -> Option<Bytes> {
+        match self.location {
+            Some(ref src) => Some(src.content.clone()),
+            None => None,
+        }
+    }
+}
+
+impl Options {
+    /// Create a default `Options`.
+    pub fn new() -> Self {
+        Self::default()
+    }
+
+    /// Append a filter. A filter can decide to ignore a config item, or change its section,
+    /// config name, or even value. The filter function takes a tuple of `(section, name, value)`
+    /// and outputs `None` to prevent inserting that value, or `Some((section, name, value))` to
+    /// insert it with optionally different name or values.
+    ///
+    /// Filters inserted first will be executed first.
+    pub fn append_filter(
+        mut self,
+        filter: Box<dyn Fn(Bytes, Bytes, Option<Bytes>) -> Option<(Bytes, Bytes, Option<Bytes>)>>,
+    ) -> Self {
+        self.filters.push(filter);
+        self
+    }
+
+    /// Set `source` information. It is about who initialized the config loading.  For example,
+    /// "user_hgrc" indicates it is from the user config file, "--config" indicates it is from the
+    /// global "--config" command line flag, "env" indicates it is translated from an environment
+    /// variable (ex.  "PAGER"), etc.
+    pub fn source<B: Into<Bytes>>(mut self, source: B) -> Self {
+        self.source = source.into();
+        self
+    }
+}
+
+/// Convert a "source" string to an `Options`.
+impl<S: Into<Bytes>> From<S> for Options {
+    fn from(source: S) -> Options {
+        Options::new().source(source.into())
+    }
+}
+
+/// Remove space characters from both ends. Remove newline characters from the end.
+/// `start` position is inclusive, `end` is exclusive.
+/// Return the stripped `start` and `end` offsets.
+#[inline]
+fn strip_offsets(buf: &Bytes, start: usize, end: usize) -> (usize, usize) {
+    let mut start = start;
+    let mut end = end;
+    while start < end && b" \t".contains(&buf[start]) {
+        start += 1
+    }
+    while start < end && b" \t\r\n".contains(&buf[end - 1]) {
+        end -= 1
+    }
+    (start, end)
+}
+
+#[inline]
+fn extract<'a>(buf: &Bytes, span: Span<'a>) -> Bytes {
+    let (start, end) = strip_offsets(buf, span.start(), span.end());
+    buf.slice(start, end)
+}
+
+#[cfg(test)]
+pub(crate) mod tests {
+    use super::*;
+    use std::io::Write;
+    use tempdir::TempDir;
+
+    #[test]
+    fn test_empty() {
+        let cfg = ConfigSet::new();
+        assert!(cfg.sections().is_empty());
+        assert!(cfg.keys("foo").is_empty());
+        assert!(cfg.get("foo", "bar").is_none());
+        assert!(cfg.get_sources("foo", "bar").is_empty());
+    }
+
+    #[test]
+    fn test_set() {
+        let mut cfg = ConfigSet::new();
+        cfg.set("y", "b", Some(b"1"), &"set1".into());
+        cfg.set("y", "b", Some(b"2"), &"set2".into());
+        cfg.set("y", "a", Some(b"3"), &"set3".into());
+        cfg.set("z", "p", Some(b"4"), &"set4".into());
+        cfg.set("z", "p", None, &"set5".into());
+        assert_eq!(cfg.sections(), vec![Bytes::from("y"), Bytes::from("z")]);
+        assert_eq!(cfg.keys("y"), vec![Bytes::from("b"), Bytes::from("a")]);
+        assert_eq!(cfg.get("y", "b"), Some(Bytes::from("2")));
+        assert_eq!(cfg.get("y", "a"), Some(Bytes::from("3")));
+        assert_eq!(cfg.get("z", "p"), None);
+
+        let sources = cfg.get_sources("z", "p");
+        assert_eq!(sources.len(), 2);
+        assert_eq!(sources[0].value(), &Some(Bytes::from("4")));
+        assert_eq!(sources[1].value(), &None);
+        assert_eq!(sources[0].source(), "set4");
+        assert_eq!(sources[1].source(), "set5");
+        assert_eq!(sources[0].location(), None);
+        assert_eq!(sources[1].location(), None);
+        assert_eq!(sources[1].file_content(), None);
+    }
+
+    #[test]
+    fn test_clone() {
+        let mut cfg = ConfigSet::new();
+        assert!(cfg.clone().sections().is_empty());
+        cfg.set("x", "a", Some(b"1"), &"set1".into());
+        assert_eq!(cfg.clone().sections(), vec![Bytes::from("x")]);
+        assert_eq!(cfg.clone().get("x", "a"), Some("1".into()));
+    }
+
+    #[test]
+    fn test_parse_basic() {
+        let mut cfg = ConfigSet::new();
+        cfg.parse(
+            "[y]\n\
+             a = 0\n\
+             b=1\n\
+             # override a to 2\n\
+             a  =  2 \n\
+             \n\
+             [x]\n\
+             m = this\n \
+             value has\r\n \
+             multi lines\n\
+             ; comment again\n\
+             n =\n",
+            &"test_parse_basic".into(),
+        );
+
+        assert_eq!(cfg.sections(), vec![Bytes::from("y"), Bytes::from("x")]);
+        assert_eq!(cfg.keys("y"), vec![Bytes::from("a"), Bytes::from("b")]);
+        assert_eq!(cfg.keys("x"), vec![Bytes::from("m"), Bytes::from("n")]);
+
+        assert_eq!(cfg.get("y", "a"), Some(Bytes::from("2")));
+        assert_eq!(cfg.get("y", "b"), Some(Bytes::from("1")));
+        assert_eq!(cfg.get("x", "n"), Some(Bytes::new()));
+        assert_eq!(
+            cfg.get("x", "m"),
+            Some(Bytes::from(&b"this\nvalue has\nmulti lines"[..]))
+        );
+
+        let sources = cfg.get_sources("y", "a");
+        assert_eq!(sources.len(), 2);
+        assert_eq!(sources[0].value(), &Some(Bytes::from("0")));
+        assert_eq!(sources[1].value(), &Some(Bytes::from("2")));
+        assert_eq!(sources[0].source(), "test_parse_basic");
+        assert_eq!(sources[1].source(), "test_parse_basic");
+        assert_eq!(sources[0].location().unwrap(), (PathBuf::new(), 8..9));
+        assert_eq!(sources[1].location().unwrap(), (PathBuf::new(), 38..40));
+        assert_eq!(sources[1].file_content().unwrap().len(), 100);
+    }
+
+    #[test]
+    fn test_parse_spaces() {
+        let mut cfg = ConfigSet::new();
+
+        cfg.parse(
+            "# space after section name\n\
+             [a]    \n\
+             # empty lines\n    \n\t\n\n\
+             x=1\n\
+             # space in config name\n\
+             y y \t =2\n\
+             # space in multi-line config value, with trailing spaces\n\
+             z=\t \n 3 3 \n  \n  4  \n\t5  \n  \n\
+             # empty values\n\
+             e1 =\n\
+             e2 = \n\
+             e3 =\n  \n\
+             \n\
+             # space in section name\n\
+             [ b c\t]\n\
+             # space in unset\n\
+             y y =\n\
+             %unset  y y \n\
+             # no space at EOF\n\
+             x=4",
+            &"".into(),
+        );
+
+        assert_eq!(cfg.get("a", "x"), Some("1".into()));
+        assert_eq!(cfg.get("a", "y y"), Some("2".into()));
+        assert_eq!(cfg.get("a", "z"), Some("\n3 3\n\n4\n5".into()));
+        assert_eq!(cfg.get("a", "e1"), Some("".into()));
+        assert_eq!(cfg.get("a", "e2"), Some("".into()));
+        assert_eq!(cfg.get("a", "e3"), Some("".into()));
+        assert_eq!(cfg.get("b c", "y y"), None);
+        assert_eq!(cfg.get("b c", "x"), Some("4".into()));
+    }
+
+    #[test]
+    fn test_corner_cases() {
+        let mut cfg = ConfigSet::new();
+        let errors = cfg.parse(
+            "# section looks like a config assignment\n\
+             [a=b]\n\
+             # comments look like config assignments\n\
+             # a = b\n\
+             ; a = b\n\
+             # multiple equal signs in a config assignment\n\
+             c = d = e\n\
+             #",
+            &"".into(),
+        );
+
+        assert_eq!(format!("{:?}", errors), "[]");
+        assert_eq!(cfg.get("a=b", "c"), Some("d = e".into()));
+        assert_eq!(cfg.get("a=b", "a"), None);
+        assert_eq!(cfg.get("a=b", "# a"), None);
+        assert_eq!(cfg.get("a=b", "; a"), None);
+    }
+
+    #[test]
+    fn test_parse_errors() {
+        let mut cfg = ConfigSet::new();
+        let errors = cfg.parse("=foo", &"test_parse_errors".into());
+        assert_eq!(
+            format!("{}", errors[0]),
+            "\"\":
+ --> 1:1
+  |
+1 | =foo
+  | ^---
+  |
+  = expected EOI, new_line, config_name, left_bracket, comment_line, or directive"
+        );
+
+        let errors = cfg.parse(" a=b", &"test_parse_errors".into());
+        assert_eq!(
+            format!("{}", errors[0]),
+            "\"\":
+ --> 1:2
+  |
+1 |  a=b
+  |  ^---
+  |
+  = expected EOI or new_line"
+        );
+
+        let errors = cfg.parse("%unset =foo", &"test_parse_errors".into());
+        assert_eq!(
+            format!("{}", errors[0]),
+            "\"\":
+ --> 1:8
+  |
+1 | %unset =foo
+  |        ^---
+  |
+  = expected space or config_name"
+        );
+
+        let errors = cfg.parse("[", &"test_parse_errors".into());
+        assert_eq!(
+            format!("{}", errors[0]),
+            "\"\":
+ --> 1:2
+  |
+1 | [
+  |  ^---
+  |
+  = expected section_name"
+        );
+
+        let errors = cfg.parse("[]", &"test_parse_errors".into());
+        assert_eq!(
+            format!("{}", errors[0]),
+            "\"\":
+ --> 1:2
+  |
+1 | []
+  |  ^---
+  |
+  = expected section_name"
+        );
+
+        let errors = cfg.parse("[a]]", &"test_parse_errors".into());
+        assert_eq!(
+            format!("{}", errors[0]),
+            "\"\":
+ --> 1:4
+  |
+1 | [a]]
+  |    ^---
+  |
+  = expected EOI, new_line, or space"
+        );
+
+        let errors = cfg.parse("# foo\n[y", &"test_parse_errors".into());
+        assert_eq!(
+            format!("{}", errors[0]),
+            "\"\":
+ --> 2:3
+  |
+2 | [y
+  |   ^---
+  |
+  = expected right_bracket"
+        );
+
+        let mut cfg = ConfigSet::new();
+        let errors = cfg.parse("\n\n%unknown", &"test_parse_errors".into());
+        assert_eq!(
+            format!("{}", errors[0]),
+            "\"\":
+ --> 3:2
+  |
+3 | %unknown
+  |  ^---
+  |
+  = expected include or unset"
+        );
+
+        let mut cfg = ConfigSet::new();
+        let errors = cfg.parse("[section]\nabc", &"test_parse_errors".into());
+        assert_eq!(
+            format!("{}", errors[0]),
+            "\"\":
+ --> 2:4
+  |
+2 | abc
+  |    ^---
+  |
+  = expected equal_sign"
+        );
+    }
+
+    #[test]
+    fn test_parse_unset() {
+        let mut cfg = ConfigSet::new();
+        cfg.parse(
+            "[x]\n\
+             a = 1\n\
+             %unset b\n\
+             b = 2\n\
+             %unset  a \n\
+             c = 3\n\
+             d = 4\n\
+             [y]\n\
+             %unset  c\n\
+             [x]\n\
+             %unset  d ",
+            &"test_parse_unset".into(),
+        );
+
+        assert_eq!(cfg.get("x", "a"), None);
+        assert_eq!(cfg.get("x", "b"), Some(Bytes::from("2")));
+        assert_eq!(cfg.get("x", "c"), Some(Bytes::from("3")));
+        assert_eq!(cfg.get("x", "d"), None);
+
+        let sources = cfg.get_sources("x", "a");
+        assert_eq!(sources.len(), 2);
+        assert_eq!(sources[0].location().unwrap(), (PathBuf::new(), 8..9));
+        assert_eq!(sources[1].location().unwrap(), (PathBuf::new(), 26..35));
+    }
+
+    #[test]
+    fn test_filters() {
+        fn blacklist_section_x(
+            section: Bytes,
+            name: Bytes,
+            value: Option<Bytes>,
+        ) -> Option<(Bytes, Bytes, Option<Bytes>)> {
+            if section.as_ref() == b"x" {
+                None
+            } else {
+                Some((section, name, value))
+            }
+        }
+
+        fn swap_name_value(
+            section: Bytes,
+            name: Bytes,
+            value: Option<Bytes>,
+        ) -> Option<(Bytes, Bytes, Option<Bytes>)> {
+            Some((section, value.unwrap(), name.into()))
+        }
+
+        fn rename_section_to_z(
+            _section: Bytes,
+            name: Bytes,
+            value: Option<Bytes>,
+        ) -> Option<(Bytes, Bytes, Option<Bytes>)> {
+            Some(("z".into(), name, value))
+        }
+
+        let mut cfg = ConfigSet::new();
+        let opts = Options::new()
+            .append_filter(Box::new(blacklist_section_x))
+            .append_filter(Box::new(swap_name_value))
+            .append_filter(Box::new(rename_section_to_z));
+        cfg.parse(
+            "[x]\n\
+             a=1\n\
+             [y]\n\
+             b=c",
+            &opts,
+        );
+        assert_eq!(cfg.get("x", "a"), None);
+        assert_eq!(cfg.get("y", "b"), None);
+        assert_eq!(cfg.get("z", "c"), Some(Bytes::from("b")));
+    }
+
+    pub(crate) fn write_file(path: PathBuf, content: &str) {
+        fs::create_dir_all(path.parent().unwrap()).unwrap();
+        let mut f = fs::File::create(path).unwrap();
+        f.write_all(content.as_bytes()).unwrap();
+    }
+
+    #[test]
+    fn test_parse_include() {
+        let dir = TempDir::new("test_parse_include").unwrap();
+        write_file(
+            dir.path().join("rootrc"),
+            "[x]\n\
+             b=1\n\
+             a=1\n\
+             %include dir/abc.rc\n\
+             %include dir/y.rc\n\
+             %include dir/loop.rc\n\
+             %include b.rc\n\
+             [y]\n\
+             b=1\n\
+             [x]\n\
+             %unset f",
+        );
+
+        write_file(dir.path().join("dir/abc.rc"), "[x]\na=2\nb=2");
+        write_file(dir.path().join("dir/y.rc"), "[y]\ny=1\n%include ../e.rc");
+        write_file(dir.path().join("dir/loop.rc"), "%include ../rootrc");
+
+        // Won't be loaded before it's not inside dir/ directly.
+        write_file(dir.path().join("dir/unused/unused.rc"), "[unused]\na=1");
+
+        // Won't be loaded before it does not have ".rc" extension.
+        write_file(dir.path().join("dir/unusedrc"), "[unused]\na=1");
+
+        // Will be loaded. `%include` shouldn't cause cycles.
+        write_file(
+            dir.path().join("b.rc"),
+            "[x]\nb=4\n\
+             %include dir/abc.rc\n\
+             %include dir/y.rc\n\
+             %include dir/loop.rc",
+        );
+
+        // Will be loaded. Shouldn't cause cycles.
+        write_file(dir.path().join("e.rc"), "[x]\ne=e\n%include f.rc");
+        write_file(
+            dir.path().join("f.rc"),
+            "[x]\nf=f\n%include e.rc\n%include rootrc",
+        );
+
+        let mut cfg = ConfigSet::new();
+        let errors = cfg.load_path(dir.path().join("rootrc"), &"test_parse_include".into());
+        assert!(errors.is_empty());
+
+        assert_eq!(cfg.sections(), vec![Bytes::from("x"), Bytes::from("y")]);
+        assert_eq!(
+            cfg.keys("x"),
+            vec![
+                Bytes::from("b"),
+                Bytes::from("a"),
+                Bytes::from("e"),
+                Bytes::from("f"),
+            ]
+        );
+        assert_eq!(cfg.get("x", "a"), Some(Bytes::from("2")));
+        assert_eq!(cfg.get("x", "b"), Some(Bytes::from("4")));
+        assert_eq!(cfg.get("x", "e"), Some(Bytes::from("e")));
+        assert_eq!(cfg.get("x", "f"), None);
+        assert_eq!(cfg.get("y", "b"), Some(Bytes::from("1")));
+    }
+
+    #[test]
+    fn test_parse_include_expand() {
+        use std::env;
+        env::set_var("FOO", "f");
+
+        let dir = TempDir::new("test_parse_include_expand").unwrap();
+        write_file(
+            dir.path().join("rootrc"),
+            "%include ./${FOO}1/$FOO/3.rc\n\
+             %include ./%FOO%2/%FOO%/4.rc\n",
+        );
+
+        write_file(dir.path().join("f1/f/3.rc"), "[x]\na=1\n");
+        write_file(dir.path().join("f2/f/4.rc"), "[y]\nb=2\n");
+
+        let mut cfg = ConfigSet::new();
+        let errors = cfg.load_path(dir.path().join("rootrc"), &"include_expand".into());
+        assert!(errors.is_empty());
+
+        assert_eq!(cfg.get("x", "a"), Some(Bytes::from("1")));
+        assert_eq!(cfg.get("y", "b"), Some(Bytes::from("2")));
+    }
+}
diff --git a/rust/hg-core/src/configparser/c_api.rs b/rust/hg-core/src/configparser/c_api.rs
new file mode 100644
--- /dev/null
+++ b/rust/hg-core/src/configparser/c_api.rs
@@ -0,0 +1,151 @@
+/*
+ * Copyright (c) Facebook, Inc. and its affiliates.
+ *
+ * This software may be used and distributed according to the terms of the
+ * GNU General Public License version 2.
+ */
+
+//! This module exports some symbols to allow calling the config parser from C/C++
+use std::ffi::{CStr, OsStr};
+use std::os::raw::c_char;
+use std::path::Path;
+use std::ptr;
+use std::slice;
+
+use bytes::Bytes;
+
+use crate::config::{ConfigSet, Options};
+use crate::error::Error;
+use crate::hg::ConfigSetHgExt;
+use crate::hg::OptionsHgExt;
+
+/// Create and return a new, empty ConfigSet
+#[no_mangle]
+pub extern "C" fn hgrc_configset_new() -> *mut ConfigSet {
+    Box::into_raw(Box::new(ConfigSet::new()))
+}
+
+/// Free a ConfigSet instance created via hgrc_configset_new().
+/// Releases all associated resources.
+#[no_mangle]
+pub extern "C" fn hgrc_configset_free(cfg: *mut ConfigSet) {
+    debug_assert!(!cfg.is_null());
+    let cfg = unsafe { Box::from_raw(cfg) };
+    drop(cfg);
+}
+
+fn errors_to_bytes(errors: Vec<Error>) -> *mut Bytes {
+    if errors.is_empty() {
+        // Success!
+        return ptr::null_mut();
+    }
+
+    // Failed; convert the errors into an error string
+    let mut error_text = String::new();
+    for (idx, err) in errors.iter().enumerate() {
+        if idx > 0 {
+            error_text.push_str("\n");
+        }
+        error_text.push_str(&err.to_string());
+    }
+
+    Box::into_raw(Box::new(error_text.into()))
+}
+
+fn load_path(cfg: &mut ConfigSet, path: &Path) -> *mut Bytes {
+    let errors = cfg.load_path(path, &Options::new().process_hgplain());
+
+    errors_to_bytes(errors)
+}
+
+/// Attempt to load and parse the config file at the specified path.
+/// If successful, returns a nullptr.
+/// Returns a Bytes object containing the error reason on failure; the
+/// error object is UTF-8 encoded text, and errors can span multiple lines.
+#[cfg(unix)]
+#[no_mangle]
+pub extern "C" fn hgrc_configset_load_path(cfg: *mut ConfigSet, path: *const c_char) -> *mut Bytes {
+    debug_assert!(!path.is_null());
+    debug_assert!(!cfg.is_null());
+
+    use std::os::unix::ffi::OsStrExt;
+
+    let path_cstr = unsafe { CStr::from_ptr(path) };
+    let path_bytes = path_cstr.to_bytes();
+    let path = Path::new(OsStr::from_bytes(&path_bytes));
+
+    let cfg = unsafe { &mut *cfg };
+
+    load_path(cfg, path)
+}
+
+/// Load system config files
+#[no_mangle]
+pub extern "C" fn hgrc_configset_load_system(cfg: *mut ConfigSet) -> *mut Bytes {
+    debug_assert!(!cfg.is_null());
+    let cfg = unsafe { &mut *cfg };
+
+    // Forces datapath to be the empty string as it doesn't
+    // appear to play a useful role in simply resolving config
+    // settings for Eden.
+    errors_to_bytes(cfg.load_system())
+}
+
+/// Load user config files
+#[no_mangle]
+pub extern "C" fn hgrc_configset_load_user(cfg: *mut ConfigSet) -> *mut Bytes {
+    debug_assert!(!cfg.is_null());
+    let cfg = unsafe { &mut *cfg };
+
+    errors_to_bytes(cfg.load_user())
+}
+
+/// Returns a Bytes object holding the configuration value for the corresponding
+/// section name and key.   If there is no matching section/key pair, returns nullptr.
+#[no_mangle]
+pub extern "C" fn hgrc_configset_get(
+    cfg: *const ConfigSet,
+    section: *const u8,
+    section_len: usize,
+    name: *const u8,
+    name_len: usize,
+) -> *mut Bytes {
+    debug_assert!(!section.is_null());
+    debug_assert!(!name.is_null());
+    debug_assert!(!cfg.is_null());
+
+    let section = unsafe { slice::from_raw_parts(section, section_len) };
+    let name = unsafe { slice::from_raw_parts(name, name_len) };
+    let cfg = unsafe { &*cfg };
+
+    match cfg.get(section, name) {
+        None => ptr::null_mut(),
+        Some(bytes) => Box::into_raw(Box::new(bytes)),
+    }
+}
+
+#[repr(C)]
+pub struct ByteData {
+    ptr: *const u8,
+    len: usize,
+}
+
+/// Returns the data pointer and length for a Bytes object, suitable for constructing
+/// a folly::ByteRange.
+#[no_mangle]
+pub extern "C" fn hgrc_bytes_data(bytes: *const Bytes) -> ByteData {
+    debug_assert!(!bytes.is_null());
+    let bytes = unsafe { &*bytes };
+    ByteData {
+        ptr: bytes.as_ptr(),
+        len: bytes.len(),
+    }
+}
+
+/// Frees a Bytes object, releasing any associated resources
+#[no_mangle]
+pub extern "C" fn hgrc_bytes_free(bytes: *mut Bytes) {
+    debug_assert!(!bytes.is_null());
+    let bytes = unsafe { Box::from_raw(bytes) };
+    drop(bytes);
+}



To: indygreg, #hg-reviewers
Cc: durin42, kevincox, mjpieters, mercurial-devel


More information about the Mercurial-devel mailing list