mirror of
https://github.com/martinvonz/jj.git
synced 2024-11-24 06:19:42 +00:00
config: extract ConfigNamePathBuf to jj-lib
I'm planning to rewrite config store layer by leveraging toml_edit instead of the config crate. It will allow us to merge config overlays in a way that deprecated keys are resolved within a layer prior to merging, for example. This patch moves ConfigNamePathBuf to jj-lib where new config API will be hosted. We'll probably extract LayeredConfigs to this module, but we'll first need to split environment dependencies from it.
This commit is contained in:
parent
6e959fa12c
commit
5ad0f3dcf6
13 changed files with 182 additions and 153 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -1913,6 +1913,7 @@ dependencies = [
|
|||
"testutils",
|
||||
"thiserror",
|
||||
"tokio",
|
||||
"toml_edit",
|
||||
"tracing",
|
||||
"version_check",
|
||||
"watchman_client",
|
||||
|
|
|
@ -56,6 +56,7 @@ use jj_lib::backend::CommitId;
|
|||
use jj_lib::backend::MergedTreeId;
|
||||
use jj_lib::backend::TreeValue;
|
||||
use jj_lib::commit::Commit;
|
||||
use jj_lib::config::ConfigNamePathBuf;
|
||||
use jj_lib::file_util;
|
||||
use jj_lib::fileset;
|
||||
use jj_lib::fileset::FilesetDiagnostics;
|
||||
|
@ -145,7 +146,6 @@ use crate::complete;
|
|||
use crate::config::new_config_path;
|
||||
use crate::config::AnnotatedValue;
|
||||
use crate::config::CommandNameAndArgs;
|
||||
use crate::config::ConfigNamePathBuf;
|
||||
use crate::config::ConfigSource;
|
||||
use crate::config::LayeredConfigs;
|
||||
use crate::diff_util;
|
||||
|
|
|
@ -15,13 +15,13 @@
|
|||
use std::io::Write as _;
|
||||
|
||||
use clap_complete::ArgValueCandidates;
|
||||
use jj_lib::config::ConfigNamePathBuf;
|
||||
use tracing::instrument;
|
||||
|
||||
use crate::cli_util::CommandHelper;
|
||||
use crate::command_error::config_error;
|
||||
use crate::command_error::CommandError;
|
||||
use crate::complete;
|
||||
use crate::config::ConfigNamePathBuf;
|
||||
use crate::ui::Ui;
|
||||
|
||||
/// Get the value of a given config option.
|
||||
|
|
|
@ -13,6 +13,7 @@
|
|||
// limitations under the License.
|
||||
|
||||
use clap_complete::ArgValueCandidates;
|
||||
use jj_lib::config::ConfigNamePathBuf;
|
||||
use tracing::instrument;
|
||||
|
||||
use super::ConfigLevelArgs;
|
||||
|
@ -21,7 +22,6 @@ use crate::command_error::CommandError;
|
|||
use crate::complete;
|
||||
use crate::config::to_toml_value;
|
||||
use crate::config::AnnotatedValue;
|
||||
use crate::config::ConfigNamePathBuf;
|
||||
use crate::config::ConfigSource;
|
||||
use crate::generic_templater::GenericTemplateLanguage;
|
||||
use crate::template_builder::TemplateLanguage as _;
|
||||
|
|
|
@ -16,6 +16,7 @@ use std::io;
|
|||
|
||||
use clap_complete::ArgValueCandidates;
|
||||
use jj_lib::commit::Commit;
|
||||
use jj_lib::config::ConfigNamePathBuf;
|
||||
use jj_lib::repo::Repo;
|
||||
use tracing::instrument;
|
||||
|
||||
|
@ -28,7 +29,6 @@ use crate::command_error::CommandError;
|
|||
use crate::complete;
|
||||
use crate::config::parse_toml_value_or_bare_string;
|
||||
use crate::config::write_config_value_to_file;
|
||||
use crate::config::ConfigNamePathBuf;
|
||||
use crate::ui::Ui;
|
||||
|
||||
/// Update config file to set the given option to a given value.
|
||||
|
|
|
@ -13,6 +13,7 @@
|
|||
// limitations under the License.
|
||||
|
||||
use clap_complete::ArgValueCandidates;
|
||||
use jj_lib::config::ConfigNamePathBuf;
|
||||
use tracing::instrument;
|
||||
|
||||
use super::ConfigLevelArgs;
|
||||
|
@ -22,7 +23,6 @@ use crate::command_error::user_error;
|
|||
use crate::command_error::CommandError;
|
||||
use crate::complete;
|
||||
use crate::config::remove_config_value_from_file;
|
||||
use crate::config::ConfigNamePathBuf;
|
||||
use crate::ui::Ui;
|
||||
|
||||
/// Update config file to unset the given option.
|
||||
|
|
|
@ -19,6 +19,7 @@ use std::num::NonZeroU32;
|
|||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use jj_lib::config::ConfigNamePathBuf;
|
||||
use jj_lib::git;
|
||||
use jj_lib::git::GitFetchError;
|
||||
use jj_lib::git::GitFetchStats;
|
||||
|
@ -34,7 +35,6 @@ use crate::command_error::user_error_with_message;
|
|||
use crate::command_error::CommandError;
|
||||
use crate::commands::git::maybe_add_gitignore;
|
||||
use crate::config::write_config_value_to_file;
|
||||
use crate::config::ConfigNamePathBuf;
|
||||
use crate::git_util::get_git_repo;
|
||||
use crate::git_util::map_git_error;
|
||||
use crate::git_util::print_git_import_stats;
|
||||
|
|
|
@ -17,6 +17,7 @@ use std::path::Path;
|
|||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
|
||||
use jj_lib::config::ConfigNamePathBuf;
|
||||
use jj_lib::file_util;
|
||||
use jj_lib::git;
|
||||
use jj_lib::git::parse_git_ref;
|
||||
|
@ -35,7 +36,6 @@ use crate::command_error::user_error_with_message;
|
|||
use crate::command_error::CommandError;
|
||||
use crate::commands::git::maybe_add_gitignore;
|
||||
use crate::config::write_config_value_to_file;
|
||||
use crate::config::ConfigNamePathBuf;
|
||||
use crate::git_util::get_git_repo;
|
||||
use crate::git_util::is_colocated_git_workspace;
|
||||
use crate::git_util::print_failed_git_export;
|
||||
|
|
|
@ -17,6 +17,7 @@ use clap::FromArgMatches as _;
|
|||
use clap_complete::CompletionCandidate;
|
||||
use config::Config;
|
||||
use itertools::Itertools;
|
||||
use jj_lib::config::ConfigNamePathBuf;
|
||||
use jj_lib::workspace::DefaultWorkspaceLoaderFactory;
|
||||
use jj_lib::workspace::WorkspaceLoaderFactory as _;
|
||||
|
||||
|
@ -26,7 +27,6 @@ use crate::cli_util::GlobalArgs;
|
|||
use crate::command_error::user_error;
|
||||
use crate::command_error::CommandError;
|
||||
use crate::config::default_config;
|
||||
use crate::config::ConfigNamePathBuf;
|
||||
use crate::config::LayeredConfigs;
|
||||
use crate::config::CONFIG_SCHEMA;
|
||||
use crate::ui::Ui;
|
||||
|
|
|
@ -20,11 +20,9 @@ use std::fmt;
|
|||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
use std::process::Command;
|
||||
use std::slice;
|
||||
use std::str::FromStr;
|
||||
|
||||
use config::Source;
|
||||
use itertools::Itertools;
|
||||
use jj_lib::config::ConfigNamePathBuf;
|
||||
use jj_lib::settings::ConfigResultExt as _;
|
||||
use regex::Captures;
|
||||
use regex::Regex;
|
||||
|
@ -85,111 +83,6 @@ pub enum ConfigError {
|
|||
ConfigCreateError(#[from] std::io::Error),
|
||||
}
|
||||
|
||||
/// Dotted config name path.
|
||||
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
|
||||
pub struct ConfigNamePathBuf(Vec<toml_edit::Key>);
|
||||
|
||||
impl ConfigNamePathBuf {
|
||||
/// Creates an empty path pointing to the root table.
|
||||
///
|
||||
/// This isn't a valid TOML key expression, but provided for convenience.
|
||||
pub fn root() -> Self {
|
||||
ConfigNamePathBuf(vec![])
|
||||
}
|
||||
|
||||
/// Returns true if the path is empty (i.e. pointing to the root table.)
|
||||
pub fn is_root(&self) -> bool {
|
||||
self.0.is_empty()
|
||||
}
|
||||
|
||||
/// Returns iterator of path components (or keys.)
|
||||
pub fn components(&self) -> slice::Iter<'_, toml_edit::Key> {
|
||||
self.0.iter()
|
||||
}
|
||||
|
||||
/// Appends the given `key` component.
|
||||
pub fn push(&mut self, key: impl Into<toml_edit::Key>) {
|
||||
self.0.push(key.into());
|
||||
}
|
||||
|
||||
/// Looks up value in the given `config`.
|
||||
///
|
||||
/// This is a workaround for the `config.get()` API, which doesn't support
|
||||
/// literal path expression. If we implement our own config abstraction,
|
||||
/// this method should be moved there.
|
||||
pub fn lookup_value(
|
||||
&self,
|
||||
config: &config::Config,
|
||||
) -> Result<config::Value, config::ConfigError> {
|
||||
// Use config.get() if the TOML keys can be converted to config path
|
||||
// syntax. This should be cheaper than cloning the whole config map.
|
||||
let (key_prefix, components) = self.split_safe_prefix();
|
||||
let value: config::Value = match &key_prefix {
|
||||
Some(key) => config.get(key)?,
|
||||
None => config.collect()?.into(),
|
||||
};
|
||||
components
|
||||
.iter()
|
||||
.try_fold(value, |value, key| {
|
||||
let mut table = value.into_table().ok()?;
|
||||
table.remove(key.get())
|
||||
})
|
||||
.ok_or_else(|| config::ConfigError::NotFound(self.to_string()))
|
||||
}
|
||||
|
||||
/// Splits path to dotted literal expression and remainder.
|
||||
///
|
||||
/// The literal expression part doesn't contain meta characters other than
|
||||
/// ".", therefore it can be passed in to `config.get()`.
|
||||
/// https://github.com/mehcode/config-rs/issues/110
|
||||
fn split_safe_prefix(&self) -> (Option<Cow<'_, str>>, &[toml_edit::Key]) {
|
||||
// https://github.com/mehcode/config-rs/blob/v0.13.4/src/path/parser.rs#L15
|
||||
let is_ident = |key: &str| {
|
||||
key.chars()
|
||||
.all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-')
|
||||
};
|
||||
let pos = self.0.iter().take_while(|&k| is_ident(k)).count();
|
||||
let safe_key = match pos {
|
||||
0 => None,
|
||||
1 => Some(Cow::Borrowed(self.0[0].get())),
|
||||
_ => Some(Cow::Owned(self.0[..pos].iter().join("."))),
|
||||
};
|
||||
(safe_key, &self.0[pos..])
|
||||
}
|
||||
}
|
||||
|
||||
impl<K: Into<toml_edit::Key>> FromIterator<K> for ConfigNamePathBuf {
|
||||
fn from_iter<I: IntoIterator<Item = K>>(iter: I) -> Self {
|
||||
let keys = iter.into_iter().map(|k| k.into()).collect();
|
||||
ConfigNamePathBuf(keys)
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for ConfigNamePathBuf {
|
||||
type Err = toml_edit::TomlError;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
// TOML parser ensures that the returned vec is not empty.
|
||||
toml_edit::Key::parse(s).map(ConfigNamePathBuf)
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<[toml_edit::Key]> for ConfigNamePathBuf {
|
||||
fn as_ref(&self) -> &[toml_edit::Key] {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for ConfigNamePathBuf {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let mut components = self.0.iter().fuse();
|
||||
if let Some(key) = components.next() {
|
||||
write!(f, "{key}")?;
|
||||
}
|
||||
components.try_for_each(|key| write!(f, ".{key}"))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum ConfigSource {
|
||||
Default,
|
||||
|
@ -829,43 +722,6 @@ mod tests {
|
|||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_split_safe_config_name_path() {
|
||||
let parse = |s| ConfigNamePathBuf::from_str(s).unwrap();
|
||||
let key = |s: &str| toml_edit::Key::new(s);
|
||||
|
||||
// Empty (or root) path isn't recognized by config::Config::get()
|
||||
assert_eq!(
|
||||
ConfigNamePathBuf::root().split_safe_prefix(),
|
||||
(None, [].as_slice())
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
parse("Foo-bar_1").split_safe_prefix(),
|
||||
(Some("Foo-bar_1".into()), [].as_slice())
|
||||
);
|
||||
assert_eq!(
|
||||
parse("'foo()'").split_safe_prefix(),
|
||||
(None, [key("foo()")].as_slice())
|
||||
);
|
||||
assert_eq!(
|
||||
parse("foo.'bar()'").split_safe_prefix(),
|
||||
(Some("foo".into()), [key("bar()")].as_slice())
|
||||
);
|
||||
assert_eq!(
|
||||
parse("foo.'bar()'.baz").split_safe_prefix(),
|
||||
(Some("foo".into()), [key("bar()"), key("baz")].as_slice())
|
||||
);
|
||||
assert_eq!(
|
||||
parse("foo.bar").split_safe_prefix(),
|
||||
(Some("foo.bar".into()), [].as_slice())
|
||||
);
|
||||
assert_eq!(
|
||||
parse("foo.bar.'baz()'").split_safe_prefix(),
|
||||
(Some("foo.bar".into()), [key("baz()")].as_slice())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_command_args() {
|
||||
let config = config::Config::builder()
|
||||
|
|
|
@ -71,6 +71,7 @@ strsim = { workspace = true }
|
|||
tempfile = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
tokio = { workspace = true, optional = true }
|
||||
toml_edit = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
watchman_client = { workspace = true, optional = true }
|
||||
whoami = { workspace = true }
|
||||
|
|
170
lib/src/config.rs
Normal file
170
lib/src/config.rs
Normal file
|
@ -0,0 +1,170 @@
|
|||
// Copyright 2022 The Jujutsu Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// https://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//! Configuration store helpers.
|
||||
|
||||
use std::borrow::Cow;
|
||||
use std::fmt;
|
||||
use std::slice;
|
||||
use std::str::FromStr;
|
||||
|
||||
use config::Source as _;
|
||||
use itertools::Itertools as _;
|
||||
|
||||
/// Dotted config name path.
|
||||
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
|
||||
pub struct ConfigNamePathBuf(Vec<toml_edit::Key>);
|
||||
|
||||
impl ConfigNamePathBuf {
|
||||
/// Creates an empty path pointing to the root table.
|
||||
///
|
||||
/// This isn't a valid TOML key expression, but provided for convenience.
|
||||
pub fn root() -> Self {
|
||||
ConfigNamePathBuf(vec![])
|
||||
}
|
||||
|
||||
/// Returns true if the path is empty (i.e. pointing to the root table.)
|
||||
pub fn is_root(&self) -> bool {
|
||||
self.0.is_empty()
|
||||
}
|
||||
|
||||
/// Returns iterator of path components (or keys.)
|
||||
pub fn components(&self) -> slice::Iter<'_, toml_edit::Key> {
|
||||
self.0.iter()
|
||||
}
|
||||
|
||||
/// Appends the given `key` component.
|
||||
pub fn push(&mut self, key: impl Into<toml_edit::Key>) {
|
||||
self.0.push(key.into());
|
||||
}
|
||||
|
||||
/// Looks up value in the given `config`.
|
||||
///
|
||||
/// This is a workaround for the `config.get()` API, which doesn't support
|
||||
/// literal path expression. If we implement our own config abstraction,
|
||||
/// this method should be moved there.
|
||||
pub fn lookup_value(
|
||||
&self,
|
||||
config: &config::Config,
|
||||
) -> Result<config::Value, config::ConfigError> {
|
||||
// Use config.get() if the TOML keys can be converted to config path
|
||||
// syntax. This should be cheaper than cloning the whole config map.
|
||||
let (key_prefix, components) = self.split_safe_prefix();
|
||||
let value: config::Value = match &key_prefix {
|
||||
Some(key) => config.get(key)?,
|
||||
None => config.collect()?.into(),
|
||||
};
|
||||
components
|
||||
.iter()
|
||||
.try_fold(value, |value, key| {
|
||||
let mut table = value.into_table().ok()?;
|
||||
table.remove(key.get())
|
||||
})
|
||||
.ok_or_else(|| config::ConfigError::NotFound(self.to_string()))
|
||||
}
|
||||
|
||||
/// Splits path to dotted literal expression and remainder.
|
||||
///
|
||||
/// The literal expression part doesn't contain meta characters other than
|
||||
/// ".", therefore it can be passed in to `config.get()`.
|
||||
/// https://github.com/mehcode/config-rs/issues/110
|
||||
fn split_safe_prefix(&self) -> (Option<Cow<'_, str>>, &[toml_edit::Key]) {
|
||||
// https://github.com/mehcode/config-rs/blob/v0.13.4/src/path/parser.rs#L15
|
||||
let is_ident = |key: &str| {
|
||||
key.chars()
|
||||
.all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-')
|
||||
};
|
||||
let pos = self.0.iter().take_while(|&k| is_ident(k)).count();
|
||||
let safe_key = match pos {
|
||||
0 => None,
|
||||
1 => Some(Cow::Borrowed(self.0[0].get())),
|
||||
_ => Some(Cow::Owned(self.0[..pos].iter().join("."))),
|
||||
};
|
||||
(safe_key, &self.0[pos..])
|
||||
}
|
||||
}
|
||||
|
||||
impl<K: Into<toml_edit::Key>> FromIterator<K> for ConfigNamePathBuf {
|
||||
fn from_iter<I: IntoIterator<Item = K>>(iter: I) -> Self {
|
||||
let keys = iter.into_iter().map(|k| k.into()).collect();
|
||||
ConfigNamePathBuf(keys)
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for ConfigNamePathBuf {
|
||||
type Err = toml_edit::TomlError;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
// TOML parser ensures that the returned vec is not empty.
|
||||
toml_edit::Key::parse(s).map(ConfigNamePathBuf)
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<[toml_edit::Key]> for ConfigNamePathBuf {
|
||||
fn as_ref(&self) -> &[toml_edit::Key] {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for ConfigNamePathBuf {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let mut components = self.0.iter().fuse();
|
||||
if let Some(key) = components.next() {
|
||||
write!(f, "{key}")?;
|
||||
}
|
||||
components.try_for_each(|key| write!(f, ".{key}"))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_split_safe_config_name_path() {
|
||||
let parse = |s| ConfigNamePathBuf::from_str(s).unwrap();
|
||||
let key = |s: &str| toml_edit::Key::new(s);
|
||||
|
||||
// Empty (or root) path isn't recognized by config::Config::get()
|
||||
assert_eq!(
|
||||
ConfigNamePathBuf::root().split_safe_prefix(),
|
||||
(None, [].as_slice())
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
parse("Foo-bar_1").split_safe_prefix(),
|
||||
(Some("Foo-bar_1".into()), [].as_slice())
|
||||
);
|
||||
assert_eq!(
|
||||
parse("'foo()'").split_safe_prefix(),
|
||||
(None, [key("foo()")].as_slice())
|
||||
);
|
||||
assert_eq!(
|
||||
parse("foo.'bar()'").split_safe_prefix(),
|
||||
(Some("foo".into()), [key("bar()")].as_slice())
|
||||
);
|
||||
assert_eq!(
|
||||
parse("foo.'bar()'.baz").split_safe_prefix(),
|
||||
(Some("foo".into()), [key("bar()"), key("baz")].as_slice())
|
||||
);
|
||||
assert_eq!(
|
||||
parse("foo.bar").split_safe_prefix(),
|
||||
(Some("foo.bar".into()), [].as_slice())
|
||||
);
|
||||
assert_eq!(
|
||||
parse("foo.bar.'baz()'").split_safe_prefix(),
|
||||
(Some("foo.bar".into()), [key("baz()")].as_slice())
|
||||
);
|
||||
}
|
||||
}
|
|
@ -32,6 +32,7 @@ pub mod annotate;
|
|||
pub mod backend;
|
||||
pub mod commit;
|
||||
pub mod commit_builder;
|
||||
pub mod config;
|
||||
pub mod conflicts;
|
||||
pub mod copies;
|
||||
pub mod dag_walk;
|
||||
|
|
Loading…
Reference in a new issue