just playing with tangled
1// Copyright 2022 The Jujutsu Authors
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// https://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use std::borrow::Cow;
16use std::collections::BTreeSet;
17use std::collections::HashMap;
18use std::env;
19use std::env::split_paths;
20use std::fmt;
21use std::path::Path;
22use std::path::PathBuf;
23use std::process::Command;
24
25use itertools::Itertools as _;
26use jj_lib::config::ConfigFile;
27use jj_lib::config::ConfigGetError;
28use jj_lib::config::ConfigLayer;
29use jj_lib::config::ConfigLoadError;
30use jj_lib::config::ConfigMigrationRule;
31use jj_lib::config::ConfigNamePathBuf;
32use jj_lib::config::ConfigResolutionContext;
33use jj_lib::config::ConfigSource;
34use jj_lib::config::ConfigValue;
35use jj_lib::config::StackedConfig;
36use regex::Captures;
37use regex::Regex;
38use tracing::instrument;
39
40use crate::command_error::config_error;
41use crate::command_error::config_error_with_message;
42use crate::command_error::CommandError;
43
44// TODO(#879): Consider generating entire schema dynamically vs. static file.
45pub const CONFIG_SCHEMA: &str = include_str!("config-schema.json");
46
47/// Parses a TOML value expression. Interprets the given value as string if it
48/// can't be parsed and doesn't look like a TOML expression.
49pub fn parse_value_or_bare_string(value_str: &str) -> Result<ConfigValue, toml_edit::TomlError> {
50 match value_str.parse() {
51 Ok(value) => Ok(value),
52 Err(_) if is_bare_string(value_str) => Ok(value_str.into()),
53 Err(err) => Err(err),
54 }
55}
56
57fn is_bare_string(value_str: &str) -> bool {
58 // leading whitespace isn't ignored when parsing TOML value expression, but
59 // "\n[]" doesn't look like a bare string.
60 let trimmed = value_str.trim_ascii().as_bytes();
61 if let (Some(&first), Some(&last)) = (trimmed.first(), trimmed.last()) {
62 // string, array, or table constructs?
63 !matches!(first, b'"' | b'\'' | b'[' | b'{') && !matches!(last, b'"' | b'\'' | b']' | b'}')
64 } else {
65 true // empty or whitespace only
66 }
67}
68
69/// Configuration variable with its source information.
70#[derive(Clone, Debug)]
71pub struct AnnotatedValue {
72 /// Dotted name path to the configuration variable.
73 pub name: ConfigNamePathBuf,
74 /// Configuration value.
75 pub value: ConfigValue,
76 /// Source of the configuration value.
77 pub source: ConfigSource,
78 /// Path to the source file, if available.
79 pub path: Option<PathBuf>,
80 /// True if this value is overridden in higher precedence layers.
81 pub is_overridden: bool,
82}
83
84/// Collects values under the given `filter_prefix` name recursively, from all
85/// layers.
86pub fn resolved_config_values(
87 stacked_config: &StackedConfig,
88 filter_prefix: &ConfigNamePathBuf,
89) -> Vec<AnnotatedValue> {
90 // Collect annotated values in reverse order and mark each value shadowed by
91 // value or table in upper layers.
92 let mut config_vals = vec![];
93 let mut upper_value_names = BTreeSet::new();
94 for layer in stacked_config.layers().iter().rev() {
95 let top_item = match layer.look_up_item(filter_prefix) {
96 Ok(Some(item)) => item,
97 Ok(None) => continue, // parent is a table, but no value found
98 Err(_) => {
99 // parent is not a table, shadows lower layers
100 upper_value_names.insert(filter_prefix.clone());
101 continue;
102 }
103 };
104 let mut config_stack = vec![(filter_prefix.clone(), top_item, false)];
105 while let Some((name, item, is_parent_overridden)) = config_stack.pop() {
106 // Cannot retain inline table formatting because inner values may be
107 // overridden independently.
108 if let Some(table) = item.as_table_like() {
109 // current table and children may be shadowed by value in upper layer
110 let is_overridden = is_parent_overridden || upper_value_names.contains(&name);
111 for (k, v) in table.iter() {
112 let mut sub_name = name.clone();
113 sub_name.push(k);
114 config_stack.push((sub_name, v, is_overridden)); // in reverse order
115 }
116 } else {
117 // current value may be shadowed by value or table in upper layer
118 let maybe_child = upper_value_names
119 .range(&name..)
120 .next()
121 .filter(|next| next.starts_with(&name));
122 let is_overridden = is_parent_overridden || maybe_child.is_some();
123 if maybe_child != Some(&name) {
124 upper_value_names.insert(name.clone());
125 }
126 let value = item
127 .clone()
128 .into_value()
129 .expect("Item::None should not exist in table");
130 config_vals.push(AnnotatedValue {
131 name,
132 value,
133 source: layer.source,
134 path: layer.path.clone(),
135 is_overridden,
136 });
137 }
138 }
139 }
140 config_vals.reverse();
141 config_vals
142}
143
144/// Newtype for unprocessed (or unresolved) [`StackedConfig`].
145///
146/// This doesn't provide any strict guarantee about the underlying config
147/// object. It just requires an explicit cast to access to the config object.
148#[derive(Clone, Debug)]
149pub struct RawConfig(StackedConfig);
150
151impl AsRef<StackedConfig> for RawConfig {
152 fn as_ref(&self) -> &StackedConfig {
153 &self.0
154 }
155}
156
157impl AsMut<StackedConfig> for RawConfig {
158 fn as_mut(&mut self) -> &mut StackedConfig {
159 &mut self.0
160 }
161}
162
163#[derive(Clone, Debug)]
164enum ConfigPath {
165 /// Existing config file path.
166 Existing(PathBuf),
167 /// Could not find any config file, but a new file can be created at the
168 /// specified location.
169 New(PathBuf),
170}
171
172impl ConfigPath {
173 fn new(path: PathBuf) -> Self {
174 if path.exists() {
175 ConfigPath::Existing(path)
176 } else {
177 ConfigPath::New(path)
178 }
179 }
180
181 fn as_path(&self) -> &Path {
182 match self {
183 ConfigPath::Existing(path) | ConfigPath::New(path) => path,
184 }
185 }
186}
187
188/// Like std::fs::create_dir_all but creates new directories to be accessible to
189/// the user only on Unix (chmod 700).
190fn create_dir_all(path: &Path) -> std::io::Result<()> {
191 let mut dir = std::fs::DirBuilder::new();
192 dir.recursive(true);
193 #[cfg(unix)]
194 {
195 use std::os::unix::fs::DirBuilderExt as _;
196 dir.mode(0o700);
197 }
198 dir.create(path)
199}
200
201// The struct exists so that we can mock certain global values in unit tests.
202#[derive(Clone, Default, Debug)]
203struct UnresolvedConfigEnv {
204 config_dir: Option<PathBuf>,
205 home_dir: Option<PathBuf>,
206 jj_config: Option<String>,
207}
208
209impl UnresolvedConfigEnv {
210 fn resolve(self) -> Vec<ConfigPath> {
211 if let Some(paths) = self.jj_config {
212 return split_paths(&paths)
213 .filter(|path| !path.as_os_str().is_empty())
214 .map(ConfigPath::new)
215 .collect();
216 }
217 let mut paths = vec![];
218 let home_config_path = self.home_dir.map(|mut home_dir| {
219 home_dir.push(".jjconfig.toml");
220 ConfigPath::new(home_dir)
221 });
222 let platform_config_path = self.config_dir.clone().map(|mut config_dir| {
223 config_dir.push("jj");
224 config_dir.push("config.toml");
225 ConfigPath::new(config_dir)
226 });
227 let platform_config_dir = self.config_dir.map(|mut config_dir| {
228 config_dir.push("jj");
229 config_dir.push("conf.d");
230 ConfigPath::new(config_dir)
231 });
232 use ConfigPath::*;
233 if let Some(path @ Existing(_)) = home_config_path {
234 paths.push(path);
235 } else if let (Some(path @ New(_)), None) = (home_config_path, &platform_config_path) {
236 paths.push(path);
237 }
238 // This should be the default config created if there's
239 // no user config and `jj config edit` is executed.
240 if let Some(path) = platform_config_path {
241 paths.push(path);
242 }
243 if let Some(path @ Existing(_)) = platform_config_dir {
244 paths.push(path);
245 }
246 paths
247 }
248}
249
250#[derive(Clone, Debug)]
251pub struct ConfigEnv {
252 home_dir: Option<PathBuf>,
253 repo_path: Option<PathBuf>,
254 user_config_paths: Vec<ConfigPath>,
255 repo_config_path: Option<ConfigPath>,
256 command: Option<String>,
257}
258
259impl ConfigEnv {
260 /// Initializes configuration loader based on environment variables.
261 pub fn from_environment() -> Self {
262 // Canonicalize home as we do canonicalize cwd in CliRunner. $HOME might
263 // point to symlink.
264 let home_dir = dirs::home_dir().map(|path| dunce::canonicalize(&path).unwrap_or(path));
265 let env = UnresolvedConfigEnv {
266 config_dir: dirs::config_dir(),
267 home_dir: home_dir.clone(),
268 jj_config: env::var("JJ_CONFIG").ok(),
269 };
270 ConfigEnv {
271 home_dir,
272 repo_path: None,
273 user_config_paths: env.resolve(),
274 repo_config_path: None,
275 command: None,
276 }
277 }
278
279 pub fn set_command_name(&mut self, command: String) {
280 self.command = Some(command);
281 }
282
283 /// Returns the paths to the user-specific config files or directories.
284 pub fn user_config_paths(&self) -> impl Iterator<Item = &Path> {
285 self.user_config_paths.iter().map(|p| p.as_path())
286 }
287
288 /// Returns the paths to the existing user-specific config files or
289 /// directories.
290 pub fn existing_user_config_paths(&self) -> impl Iterator<Item = &Path> {
291 self.user_config_paths.iter().filter_map(|p| match p {
292 ConfigPath::Existing(path) => Some(path.as_path()),
293 _ => None,
294 })
295 }
296
297 /// Returns user configuration files for modification. Instantiates one if
298 /// `config` has no user configuration layers.
299 ///
300 /// The parent directory for the new file may be created by this function.
301 /// If the user configuration path is unknown, this function returns an
302 /// empty `Vec`.
303 pub fn user_config_files(
304 &self,
305 config: &RawConfig,
306 ) -> Result<Vec<ConfigFile>, ConfigLoadError> {
307 config_files_for(config, ConfigSource::User, || self.new_user_config_file())
308 }
309
310 fn new_user_config_file(&self) -> Result<Option<ConfigFile>, ConfigLoadError> {
311 self.user_config_paths()
312 .next()
313 .map(|path| {
314 // No need to propagate io::Error here. If the directory
315 // couldn't be created, file.save() would fail later.
316 if let Some(dir) = path.parent() {
317 create_dir_all(dir).ok();
318 }
319 // The path doesn't usually exist, but we shouldn't overwrite it
320 // with an empty config if it did exist.
321 ConfigFile::load_or_empty(ConfigSource::User, path)
322 })
323 .transpose()
324 }
325
326 /// Loads user-specific config files into the given `config`. The old
327 /// user-config layers will be replaced if any.
328 #[instrument]
329 pub fn reload_user_config(&self, config: &mut RawConfig) -> Result<(), ConfigLoadError> {
330 config.as_mut().remove_layers(ConfigSource::User);
331 for path in self.existing_user_config_paths() {
332 if path.is_dir() {
333 config.as_mut().load_dir(ConfigSource::User, path)?;
334 } else {
335 config.as_mut().load_file(ConfigSource::User, path)?;
336 }
337 }
338 Ok(())
339 }
340
341 /// Sets the directory where repo-specific config file is stored. The path
342 /// is usually `.jj/repo`.
343 pub fn reset_repo_path(&mut self, path: &Path) {
344 self.repo_path = Some(path.to_owned());
345 self.repo_config_path = Some(ConfigPath::new(path.join("config.toml")));
346 }
347
348 /// Returns a path to the repo-specific config file.
349 pub fn repo_config_path(&self) -> Option<&Path> {
350 self.repo_config_path.as_ref().map(ConfigPath::as_path)
351 }
352
353 /// Returns a path to the existing repo-specific config file.
354 fn existing_repo_config_path(&self) -> Option<&Path> {
355 match &self.repo_config_path {
356 Some(ConfigPath::Existing(path)) => Some(path),
357 _ => None,
358 }
359 }
360
361 /// Returns repo configuration files for modification. Instantiates one if
362 /// `config` has no repo configuration layers.
363 ///
364 /// If the repo path is unknown, this function returns an empty `Vec`. Since
365 /// the repo config path cannot be a directory, the returned `Vec` should
366 /// have at most one config file.
367 pub fn repo_config_files(
368 &self,
369 config: &RawConfig,
370 ) -> Result<Vec<ConfigFile>, ConfigLoadError> {
371 config_files_for(config, ConfigSource::Repo, || self.new_repo_config_file())
372 }
373
374 fn new_repo_config_file(&self) -> Result<Option<ConfigFile>, ConfigLoadError> {
375 self.repo_config_path()
376 // The path doesn't usually exist, but we shouldn't overwrite it
377 // with an empty config if it did exist.
378 .map(|path| ConfigFile::load_or_empty(ConfigSource::Repo, path))
379 .transpose()
380 }
381
382 /// Loads repo-specific config file into the given `config`. The old
383 /// repo-config layer will be replaced if any.
384 #[instrument]
385 pub fn reload_repo_config(&self, config: &mut RawConfig) -> Result<(), ConfigLoadError> {
386 config.as_mut().remove_layers(ConfigSource::Repo);
387 if let Some(path) = self.existing_repo_config_path() {
388 config.as_mut().load_file(ConfigSource::Repo, path)?;
389 }
390 Ok(())
391 }
392
393 /// Resolves conditional scopes within the current environment. Returns new
394 /// resolved config.
395 pub fn resolve_config(&self, config: &RawConfig) -> Result<StackedConfig, ConfigGetError> {
396 let context = ConfigResolutionContext {
397 home_dir: self.home_dir.as_deref(),
398 repo_path: self.repo_path.as_deref(),
399 command: self.command.as_deref(),
400 };
401 jj_lib::config::resolve(config.as_ref(), &context)
402 }
403}
404
405fn config_files_for(
406 config: &RawConfig,
407 source: ConfigSource,
408 new_file: impl FnOnce() -> Result<Option<ConfigFile>, ConfigLoadError>,
409) -> Result<Vec<ConfigFile>, ConfigLoadError> {
410 let mut files = config
411 .as_ref()
412 .layers_for(source)
413 .iter()
414 .filter_map(|layer| ConfigFile::from_layer(layer.clone()).ok())
415 .collect_vec();
416 if files.is_empty() {
417 files.extend(new_file()?);
418 }
419 Ok(files)
420}
421
422/// Initializes stacked config with the given `default_layers` and infallible
423/// sources.
424///
425/// Sources from the lowest precedence:
426/// 1. Default
427/// 2. Base environment variables
428/// 3. [User configs](https://jj-vcs.github.io/jj/latest/config/)
429/// 4. Repo config `.jj/repo/config.toml`
430/// 5. TODO: Workspace config `.jj/config.toml`
431/// 6. Override environment variables
432/// 7. Command-line arguments `--config`, `--config-toml`, `--config-file`
433///
434/// This function sets up 1, 2, and 6.
435pub fn config_from_environment(default_layers: impl IntoIterator<Item = ConfigLayer>) -> RawConfig {
436 let mut config = StackedConfig::with_defaults();
437 config.extend_layers(default_layers);
438 config.add_layer(env_base_layer());
439 config.add_layer(env_overrides_layer());
440 RawConfig(config)
441}
442
443const OP_HOSTNAME: &str = "operation.hostname";
444const OP_USERNAME: &str = "operation.username";
445
446/// Environment variables that should be overridden by config values
447fn env_base_layer() -> ConfigLayer {
448 let mut layer = ConfigLayer::empty(ConfigSource::EnvBase);
449 if let Ok(value) = whoami::fallible::hostname()
450 .inspect_err(|err| tracing::warn!(?err, "failed to get hostname"))
451 {
452 layer.set_value(OP_HOSTNAME, value).unwrap();
453 }
454 if let Ok(value) = whoami::fallible::username()
455 .inspect_err(|err| tracing::warn!(?err, "failed to get username"))
456 {
457 layer.set_value(OP_USERNAME, value).unwrap();
458 } else if let Ok(value) = env::var("USER") {
459 // On Unix, $USER is set by login(1). Use it as a fallback because
460 // getpwuid() of musl libc appears not (fully?) supporting nsswitch.
461 layer.set_value(OP_USERNAME, value).unwrap();
462 }
463 if !env::var("NO_COLOR").unwrap_or_default().is_empty() {
464 // "User-level configuration files and per-instance command-line arguments
465 // should override $NO_COLOR." https://no-color.org/
466 layer.set_value("ui.color", "never").unwrap();
467 }
468 if let Ok(value) = env::var("PAGER") {
469 layer.set_value("ui.pager", value).unwrap();
470 }
471 if let Ok(value) = env::var("VISUAL") {
472 layer.set_value("ui.editor", value).unwrap();
473 } else if let Ok(value) = env::var("EDITOR") {
474 layer.set_value("ui.editor", value).unwrap();
475 }
476 layer
477}
478
479pub fn default_config_layers() -> Vec<ConfigLayer> {
480 // Syntax error in default config isn't a user error. That's why defaults are
481 // loaded by separate builder.
482 let parse = |text: &'static str| ConfigLayer::parse(ConfigSource::Default, text).unwrap();
483 let mut layers = vec![
484 parse(include_str!("config/colors.toml")),
485 parse(include_str!("config/hints.toml")),
486 parse(include_str!("config/merge_tools.toml")),
487 parse(include_str!("config/misc.toml")),
488 parse(include_str!("config/revsets.toml")),
489 parse(include_str!("config/templates.toml")),
490 ];
491 if cfg!(unix) {
492 layers.push(parse(include_str!("config/unix.toml")));
493 }
494 if cfg!(windows) {
495 layers.push(parse(include_str!("config/windows.toml")));
496 }
497 layers
498}
499
500/// Environment variables that override config values
501fn env_overrides_layer() -> ConfigLayer {
502 let mut layer = ConfigLayer::empty(ConfigSource::EnvOverrides);
503 if let Ok(value) = env::var("JJ_USER") {
504 layer.set_value("user.name", value).unwrap();
505 }
506 if let Ok(value) = env::var("JJ_EMAIL") {
507 layer.set_value("user.email", value).unwrap();
508 }
509 if let Ok(value) = env::var("JJ_TIMESTAMP") {
510 layer.set_value("debug.commit-timestamp", value).unwrap();
511 }
512 if let Ok(Ok(value)) = env::var("JJ_RANDOMNESS_SEED").map(|s| s.parse::<i64>()) {
513 layer.set_value("debug.randomness-seed", value).unwrap();
514 }
515 if let Ok(value) = env::var("JJ_OP_TIMESTAMP") {
516 layer.set_value("debug.operation-timestamp", value).unwrap();
517 }
518 if let Ok(value) = env::var("JJ_OP_HOSTNAME") {
519 layer.set_value(OP_HOSTNAME, value).unwrap();
520 }
521 if let Ok(value) = env::var("JJ_OP_USERNAME") {
522 layer.set_value(OP_USERNAME, value).unwrap();
523 }
524 if let Ok(value) = env::var("JJ_EDITOR") {
525 layer.set_value("ui.editor", value).unwrap();
526 }
527 layer
528}
529
530/// Configuration source/data type provided as command-line argument.
531#[derive(Clone, Copy, Debug, Eq, PartialEq)]
532pub enum ConfigArgKind {
533 /// `--config=NAME=VALUE`
534 Item,
535 /// `--config-toml=TOML`
536 Toml,
537 /// `--config-file=PATH`
538 File,
539}
540
541/// Parses `--config*` arguments.
542pub fn parse_config_args(
543 toml_strs: &[(ConfigArgKind, &str)],
544) -> Result<Vec<ConfigLayer>, CommandError> {
545 let source = ConfigSource::CommandArg;
546 let mut layers = Vec::new();
547 for (kind, chunk) in &toml_strs.iter().chunk_by(|&(kind, _)| kind) {
548 match kind {
549 ConfigArgKind::Item => {
550 let mut layer = ConfigLayer::empty(source);
551 for (_, item) in chunk {
552 let (name, value) = parse_config_arg_item(item)?;
553 // Can fail depending on the argument order, but that
554 // wouldn't matter in practice.
555 layer.set_value(name, value).map_err(|err| {
556 config_error_with_message("--config argument cannot be set", err)
557 })?;
558 }
559 layers.push(layer);
560 }
561 ConfigArgKind::Toml => {
562 for (_, text) in chunk {
563 layers.push(ConfigLayer::parse(source, text)?);
564 }
565 }
566 ConfigArgKind::File => {
567 for (_, path) in chunk {
568 layers.push(ConfigLayer::load_from_file(source, path.into())?);
569 }
570 }
571 }
572 }
573 Ok(layers)
574}
575
576/// Parses `NAME=VALUE` string.
577fn parse_config_arg_item(item_str: &str) -> Result<(ConfigNamePathBuf, ConfigValue), CommandError> {
578 // split NAME=VALUE at the first parsable position
579 let split_candidates = item_str.as_bytes().iter().positions(|&b| b == b'=');
580 let Some((name, value_str)) = split_candidates
581 .map(|p| (&item_str[..p], &item_str[p + 1..]))
582 .map(|(name, value)| name.parse().map(|name| (name, value)))
583 .find_or_last(Result::is_ok)
584 .transpose()
585 .map_err(|err| config_error_with_message("--config name cannot be parsed", err))?
586 else {
587 return Err(config_error("--config must be specified as NAME=VALUE"));
588 };
589 let value = parse_value_or_bare_string(value_str)
590 .map_err(|err| config_error_with_message("--config value cannot be parsed", err))?;
591 Ok((name, value))
592}
593
594/// List of rules to migrate deprecated config variables.
595pub fn default_config_migrations() -> Vec<ConfigMigrationRule> {
596 vec![
597 // TODO: Delete in jj 0.32+
598 ConfigMigrationRule::rename_value("git.auto-local-branch", "git.auto-local-bookmark"),
599 // TODO: Delete in jj 0.33+
600 ConfigMigrationRule::rename_update_value(
601 "signing.sign-all",
602 "signing.behavior",
603 |old_value| {
604 if old_value
605 .as_bool()
606 .ok_or("signing.sign-all expects a boolean")?
607 {
608 Ok("own".into())
609 } else {
610 Ok("keep".into())
611 }
612 },
613 ),
614 // TODO: Delete in jj 0.34+
615 ConfigMigrationRule::rename_value(
616 "core.watchman.register_snapshot_trigger",
617 "core.watchman.register-snapshot-trigger",
618 ),
619 // TODO: Delete in jj 0.34+
620 ConfigMigrationRule::rename_value("diff.format", "ui.diff.format"),
621 // TODO: Delete with the `git.subprocess` setting.
622 #[cfg(not(feature = "git2"))]
623 ConfigMigrationRule::custom(
624 |layer| {
625 let Ok(Some(subprocess)) = layer.look_up_item("git.subprocess") else {
626 return false;
627 };
628 subprocess.as_bool() == Some(false)
629 },
630 |_| Ok("jj was compiled without `git.subprocess = false` support".into()),
631 ),
632 ]
633}
634
635/// Command name and arguments specified by config.
636#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize)]
637#[serde(untagged)]
638pub enum CommandNameAndArgs {
639 String(String),
640 Vec(NonEmptyCommandArgsVec),
641 Structured {
642 env: HashMap<String, String>,
643 command: NonEmptyCommandArgsVec,
644 },
645}
646
647impl CommandNameAndArgs {
648 /// Returns command name without arguments.
649 pub fn split_name(&self) -> Cow<str> {
650 let (name, _) = self.split_name_and_args();
651 name
652 }
653
654 /// Returns command name and arguments.
655 ///
656 /// The command name may be an empty string (as well as each argument.)
657 pub fn split_name_and_args(&self) -> (Cow<str>, Cow<[String]>) {
658 match self {
659 CommandNameAndArgs::String(s) => {
660 // Handle things like `EDITOR=emacs -nw` (TODO: parse shell escapes)
661 let mut args = s.split(' ').map(|s| s.to_owned());
662 (args.next().unwrap().into(), args.collect())
663 }
664 CommandNameAndArgs::Vec(NonEmptyCommandArgsVec(a)) => {
665 (Cow::Borrowed(&a[0]), Cow::Borrowed(&a[1..]))
666 }
667 CommandNameAndArgs::Structured {
668 env: _,
669 command: cmd,
670 } => (Cow::Borrowed(&cmd.0[0]), Cow::Borrowed(&cmd.0[1..])),
671 }
672 }
673
674 /// Returns process builder configured with this.
675 pub fn to_command(&self) -> Command {
676 let empty: HashMap<&str, &str> = HashMap::new();
677 self.to_command_with_variables(&empty)
678 }
679
680 /// Returns process builder configured with this after interpolating
681 /// variables into the arguments.
682 pub fn to_command_with_variables<V: AsRef<str>>(
683 &self,
684 variables: &HashMap<&str, V>,
685 ) -> Command {
686 let (name, args) = self.split_name_and_args();
687 let mut cmd = Command::new(name.as_ref());
688 if let CommandNameAndArgs::Structured { env, .. } = self {
689 cmd.envs(env);
690 }
691 cmd.args(interpolate_variables(&args, variables));
692 cmd
693 }
694}
695
696impl<T: AsRef<str> + ?Sized> From<&T> for CommandNameAndArgs {
697 fn from(s: &T) -> Self {
698 CommandNameAndArgs::String(s.as_ref().to_owned())
699 }
700}
701
702impl fmt::Display for CommandNameAndArgs {
703 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
704 match self {
705 CommandNameAndArgs::String(s) => write!(f, "{s}"),
706 // TODO: format with shell escapes
707 CommandNameAndArgs::Vec(a) => write!(f, "{}", a.0.join(" ")),
708 CommandNameAndArgs::Structured { env, command } => {
709 for (k, v) in env {
710 write!(f, "{k}={v} ")?;
711 }
712 write!(f, "{}", command.0.join(" "))
713 }
714 }
715 }
716}
717
718// Not interested in $UPPER_CASE_VARIABLES
719static VARIABLE_REGEX: once_cell::sync::Lazy<Regex> =
720 once_cell::sync::Lazy::new(|| Regex::new(r"\$([a-z0-9_]+)\b").unwrap());
721
722pub fn interpolate_variables<V: AsRef<str>>(
723 args: &[String],
724 variables: &HashMap<&str, V>,
725) -> Vec<String> {
726 args.iter()
727 .map(|arg| {
728 VARIABLE_REGEX
729 .replace_all(arg, |caps: &Captures| {
730 let name = &caps[1];
731 if let Some(subst) = variables.get(name) {
732 subst.as_ref().to_owned()
733 } else {
734 caps[0].to_owned()
735 }
736 })
737 .into_owned()
738 })
739 .collect()
740}
741
742/// Return all variable names found in the args, without the dollar sign
743pub fn find_all_variables(args: &[String]) -> impl Iterator<Item = &str> {
744 let regex = &*VARIABLE_REGEX;
745 args.iter()
746 .flat_map(|arg| regex.find_iter(arg))
747 .map(|single_match| {
748 let s = single_match.as_str();
749 &s[1..]
750 })
751}
752
753/// Wrapper to reject an array without command name.
754// Based on https://github.com/serde-rs/serde/issues/939
755#[derive(Clone, Debug, Eq, Hash, PartialEq, serde::Deserialize)]
756#[serde(try_from = "Vec<String>")]
757pub struct NonEmptyCommandArgsVec(Vec<String>);
758
759impl TryFrom<Vec<String>> for NonEmptyCommandArgsVec {
760 type Error = &'static str;
761
762 fn try_from(args: Vec<String>) -> Result<Self, Self::Error> {
763 if args.is_empty() {
764 Err("command arguments should not be empty")
765 } else {
766 Ok(NonEmptyCommandArgsVec(args))
767 }
768 }
769}
770
771#[cfg(test)]
772mod tests {
773 use std::env::join_paths;
774 use std::fmt::Write as _;
775
776 use indoc::indoc;
777 use maplit::hashmap;
778 use test_case::test_case;
779
780 use super::*;
781
782 fn insta_settings() -> insta::Settings {
783 let mut settings = insta::Settings::clone_current();
784 // Suppress Decor { .. } which is uninteresting
785 settings.add_filter(r"\bDecor \{[^}]*\}", "Decor { .. }");
786 settings
787 }
788
789 #[test]
790 fn test_parse_value_or_bare_string() {
791 let parse = |s: &str| parse_value_or_bare_string(s);
792
793 // Value in TOML syntax
794 assert_eq!(parse("true").unwrap().as_bool(), Some(true));
795 assert_eq!(parse("42").unwrap().as_integer(), Some(42));
796 assert_eq!(parse("-1").unwrap().as_integer(), Some(-1));
797 assert_eq!(parse("'a'").unwrap().as_str(), Some("a"));
798 assert!(parse("[]").unwrap().is_array());
799 assert!(parse("{ a = 'b' }").unwrap().is_inline_table());
800
801 // Bare string
802 assert_eq!(parse("").unwrap().as_str(), Some(""));
803 assert_eq!(parse("John Doe").unwrap().as_str(), Some("John Doe"));
804 assert_eq!(parse("Doe, John").unwrap().as_str(), Some("Doe, John"));
805 assert_eq!(parse("It's okay").unwrap().as_str(), Some("It's okay"));
806 assert_eq!(
807 parse("<foo+bar@example.org>").unwrap().as_str(),
808 Some("<foo+bar@example.org>")
809 );
810 assert_eq!(parse("#ff00aa").unwrap().as_str(), Some("#ff00aa"));
811 assert_eq!(parse("all()").unwrap().as_str(), Some("all()"));
812 assert_eq!(parse("glob:*.*").unwrap().as_str(), Some("glob:*.*"));
813 assert_eq!(parse("柔術").unwrap().as_str(), Some("柔術"));
814
815 // Error in TOML value
816 assert!(parse("'foo").is_err());
817 assert!(parse(r#" bar" "#).is_err());
818 assert!(parse("[0 1]").is_err());
819 assert!(parse("{ x = }").is_err());
820 assert!(parse("\n { x").is_err());
821 assert!(parse(" x ] ").is_err());
822 assert!(parse("[table]\nkey = 'value'").is_err());
823 }
824
825 #[test]
826 fn test_parse_config_arg_item() {
827 assert!(parse_config_arg_item("").is_err());
828 assert!(parse_config_arg_item("a").is_err());
829 assert!(parse_config_arg_item("=").is_err());
830 // The value parser is sensitive to leading whitespaces, which seems
831 // good because the parsing falls back to a bare string.
832 assert!(parse_config_arg_item("a = 'b'").is_err());
833
834 let (name, value) = parse_config_arg_item("a=b").unwrap();
835 assert_eq!(name, ConfigNamePathBuf::from_iter(["a"]));
836 assert_eq!(value.as_str(), Some("b"));
837
838 let (name, value) = parse_config_arg_item("a=").unwrap();
839 assert_eq!(name, ConfigNamePathBuf::from_iter(["a"]));
840 assert_eq!(value.as_str(), Some(""));
841
842 let (name, value) = parse_config_arg_item("a= ").unwrap();
843 assert_eq!(name, ConfigNamePathBuf::from_iter(["a"]));
844 assert_eq!(value.as_str(), Some(" "));
845
846 // This one is a bit cryptic, but b=c can be a bare string.
847 let (name, value) = parse_config_arg_item("a=b=c").unwrap();
848 assert_eq!(name, ConfigNamePathBuf::from_iter(["a"]));
849 assert_eq!(value.as_str(), Some("b=c"));
850
851 let (name, value) = parse_config_arg_item("a.b=true").unwrap();
852 assert_eq!(name, ConfigNamePathBuf::from_iter(["a", "b"]));
853 assert_eq!(value.as_bool(), Some(true));
854
855 let (name, value) = parse_config_arg_item("a='b=c'").unwrap();
856 assert_eq!(name, ConfigNamePathBuf::from_iter(["a"]));
857 assert_eq!(value.as_str(), Some("b=c"));
858
859 let (name, value) = parse_config_arg_item("'a=b'=c").unwrap();
860 assert_eq!(name, ConfigNamePathBuf::from_iter(["a=b"]));
861 assert_eq!(value.as_str(), Some("c"));
862
863 let (name, value) = parse_config_arg_item("'a = b=c '={d = 'e=f'}").unwrap();
864 assert_eq!(name, ConfigNamePathBuf::from_iter(["a = b=c "]));
865 assert!(value.is_inline_table());
866 assert_eq!(value.to_string(), "{d = 'e=f'}");
867 }
868
869 #[test]
870 fn test_command_args() {
871 let mut config = StackedConfig::empty();
872 config.add_layer(
873 ConfigLayer::parse(
874 ConfigSource::User,
875 indoc! {"
876 empty_array = []
877 empty_string = ''
878 array = ['emacs', '-nw']
879 string = 'emacs -nw'
880 structured.env = { KEY1 = 'value1', KEY2 = 'value2' }
881 structured.command = ['emacs', '-nw']
882 "},
883 )
884 .unwrap(),
885 );
886
887 assert!(config.get::<CommandNameAndArgs>("empty_array").is_err());
888
889 let command_args: CommandNameAndArgs = config.get("empty_string").unwrap();
890 assert_eq!(command_args, CommandNameAndArgs::String("".to_owned()));
891 let (name, args) = command_args.split_name_and_args();
892 assert_eq!(name, "");
893 assert!(args.is_empty());
894
895 let command_args: CommandNameAndArgs = config.get("array").unwrap();
896 assert_eq!(
897 command_args,
898 CommandNameAndArgs::Vec(NonEmptyCommandArgsVec(
899 ["emacs", "-nw",].map(|s| s.to_owned()).to_vec()
900 ))
901 );
902 let (name, args) = command_args.split_name_and_args();
903 assert_eq!(name, "emacs");
904 assert_eq!(args, ["-nw"].as_ref());
905
906 let command_args: CommandNameAndArgs = config.get("string").unwrap();
907 assert_eq!(
908 command_args,
909 CommandNameAndArgs::String("emacs -nw".to_owned())
910 );
911 let (name, args) = command_args.split_name_and_args();
912 assert_eq!(name, "emacs");
913 assert_eq!(args, ["-nw"].as_ref());
914
915 let command_args: CommandNameAndArgs = config.get("structured").unwrap();
916 assert_eq!(
917 command_args,
918 CommandNameAndArgs::Structured {
919 env: hashmap! {
920 "KEY1".to_string() => "value1".to_string(),
921 "KEY2".to_string() => "value2".to_string(),
922 },
923 command: NonEmptyCommandArgsVec(["emacs", "-nw",].map(|s| s.to_owned()).to_vec())
924 }
925 );
926 let (name, args) = command_args.split_name_and_args();
927 assert_eq!(name, "emacs");
928 assert_eq!(args, ["-nw"].as_ref());
929 }
930
931 #[test]
932 fn test_resolved_config_values_empty() {
933 let config = StackedConfig::empty();
934 assert!(resolved_config_values(&config, &ConfigNamePathBuf::root()).is_empty());
935 }
936
937 #[test]
938 fn test_resolved_config_values_single_key() {
939 let settings = insta_settings();
940 let _guard = settings.bind_to_scope();
941 let mut env_base_layer = ConfigLayer::empty(ConfigSource::EnvBase);
942 env_base_layer
943 .set_value("user.name", "base-user-name")
944 .unwrap();
945 env_base_layer
946 .set_value("user.email", "base@user.email")
947 .unwrap();
948 let mut repo_layer = ConfigLayer::empty(ConfigSource::Repo);
949 repo_layer
950 .set_value("user.email", "repo@user.email")
951 .unwrap();
952 let mut config = StackedConfig::empty();
953 config.add_layer(env_base_layer);
954 config.add_layer(repo_layer);
955 // Note: "email" is alphabetized, before "name" from same layer.
956 insta::assert_debug_snapshot!(
957 resolved_config_values(&config, &ConfigNamePathBuf::root()),
958 @r#"
959 [
960 AnnotatedValue {
961 name: ConfigNamePathBuf(
962 [
963 Key {
964 key: "user",
965 repr: None,
966 leaf_decor: Decor { .. },
967 dotted_decor: Decor { .. },
968 },
969 Key {
970 key: "name",
971 repr: None,
972 leaf_decor: Decor { .. },
973 dotted_decor: Decor { .. },
974 },
975 ],
976 ),
977 value: String(
978 Formatted {
979 value: "base-user-name",
980 repr: "default",
981 decor: Decor { .. },
982 },
983 ),
984 source: EnvBase,
985 path: None,
986 is_overridden: false,
987 },
988 AnnotatedValue {
989 name: ConfigNamePathBuf(
990 [
991 Key {
992 key: "user",
993 repr: None,
994 leaf_decor: Decor { .. },
995 dotted_decor: Decor { .. },
996 },
997 Key {
998 key: "email",
999 repr: None,
1000 leaf_decor: Decor { .. },
1001 dotted_decor: Decor { .. },
1002 },
1003 ],
1004 ),
1005 value: String(
1006 Formatted {
1007 value: "base@user.email",
1008 repr: "default",
1009 decor: Decor { .. },
1010 },
1011 ),
1012 source: EnvBase,
1013 path: None,
1014 is_overridden: true,
1015 },
1016 AnnotatedValue {
1017 name: ConfigNamePathBuf(
1018 [
1019 Key {
1020 key: "user",
1021 repr: None,
1022 leaf_decor: Decor { .. },
1023 dotted_decor: Decor { .. },
1024 },
1025 Key {
1026 key: "email",
1027 repr: None,
1028 leaf_decor: Decor { .. },
1029 dotted_decor: Decor { .. },
1030 },
1031 ],
1032 ),
1033 value: String(
1034 Formatted {
1035 value: "repo@user.email",
1036 repr: "default",
1037 decor: Decor { .. },
1038 },
1039 ),
1040 source: Repo,
1041 path: None,
1042 is_overridden: false,
1043 },
1044 ]
1045 "#
1046 );
1047 }
1048
1049 #[test]
1050 fn test_resolved_config_values_filter_path() {
1051 let settings = insta_settings();
1052 let _guard = settings.bind_to_scope();
1053 let mut user_layer = ConfigLayer::empty(ConfigSource::User);
1054 user_layer.set_value("test-table1.foo", "user-FOO").unwrap();
1055 user_layer.set_value("test-table2.bar", "user-BAR").unwrap();
1056 let mut repo_layer = ConfigLayer::empty(ConfigSource::Repo);
1057 repo_layer.set_value("test-table1.bar", "repo-BAR").unwrap();
1058 let mut config = StackedConfig::empty();
1059 config.add_layer(user_layer);
1060 config.add_layer(repo_layer);
1061 insta::assert_debug_snapshot!(
1062 resolved_config_values(&config, &ConfigNamePathBuf::from_iter(["test-table1"])),
1063 @r#"
1064 [
1065 AnnotatedValue {
1066 name: ConfigNamePathBuf(
1067 [
1068 Key {
1069 key: "test-table1",
1070 repr: None,
1071 leaf_decor: Decor { .. },
1072 dotted_decor: Decor { .. },
1073 },
1074 Key {
1075 key: "foo",
1076 repr: None,
1077 leaf_decor: Decor { .. },
1078 dotted_decor: Decor { .. },
1079 },
1080 ],
1081 ),
1082 value: String(
1083 Formatted {
1084 value: "user-FOO",
1085 repr: "default",
1086 decor: Decor { .. },
1087 },
1088 ),
1089 source: User,
1090 path: None,
1091 is_overridden: false,
1092 },
1093 AnnotatedValue {
1094 name: ConfigNamePathBuf(
1095 [
1096 Key {
1097 key: "test-table1",
1098 repr: None,
1099 leaf_decor: Decor { .. },
1100 dotted_decor: Decor { .. },
1101 },
1102 Key {
1103 key: "bar",
1104 repr: None,
1105 leaf_decor: Decor { .. },
1106 dotted_decor: Decor { .. },
1107 },
1108 ],
1109 ),
1110 value: String(
1111 Formatted {
1112 value: "repo-BAR",
1113 repr: "default",
1114 decor: Decor { .. },
1115 },
1116 ),
1117 source: Repo,
1118 path: None,
1119 is_overridden: false,
1120 },
1121 ]
1122 "#
1123 );
1124 }
1125
1126 #[test]
1127 fn test_resolved_config_values_overridden() {
1128 let list = |layers: &[&ConfigLayer], prefix: &str| -> String {
1129 let mut config = StackedConfig::empty();
1130 config.extend_layers(layers.iter().copied().cloned());
1131 let prefix = if prefix.is_empty() {
1132 ConfigNamePathBuf::root()
1133 } else {
1134 prefix.parse().unwrap()
1135 };
1136 let mut output = String::new();
1137 for annotated in resolved_config_values(&config, &prefix) {
1138 let AnnotatedValue { name, value, .. } = &annotated;
1139 let sigil = if annotated.is_overridden { '!' } else { ' ' };
1140 writeln!(output, "{sigil}{name} = {value}").unwrap();
1141 }
1142 output
1143 };
1144
1145 let mut layer0 = ConfigLayer::empty(ConfigSource::User);
1146 layer0.set_value("a.b.e", "0.0").unwrap();
1147 layer0.set_value("a.b.c.f", "0.1").unwrap();
1148 layer0.set_value("a.b.d", "0.2").unwrap();
1149 let mut layer1 = ConfigLayer::empty(ConfigSource::User);
1150 layer1.set_value("a.b", "1.0").unwrap();
1151 layer1.set_value("a.c", "1.1").unwrap();
1152 let mut layer2 = ConfigLayer::empty(ConfigSource::User);
1153 layer2.set_value("a.b.g", "2.0").unwrap();
1154 layer2.set_value("a.b.d", "2.1").unwrap();
1155
1156 // a.b.* is shadowed by a.b
1157 let layers = [&layer0, &layer1];
1158 insta::assert_snapshot!(list(&layers, ""), @r#"
1159 !a.b.e = "0.0"
1160 !a.b.c.f = "0.1"
1161 !a.b.d = "0.2"
1162 a.b = "1.0"
1163 a.c = "1.1"
1164 "#);
1165 insta::assert_snapshot!(list(&layers, "a.b"), @r#"
1166 !a.b.e = "0.0"
1167 !a.b.c.f = "0.1"
1168 !a.b.d = "0.2"
1169 a.b = "1.0"
1170 "#);
1171 insta::assert_snapshot!(list(&layers, "a.b.c"), @r#"!a.b.c.f = "0.1""#);
1172 insta::assert_snapshot!(list(&layers, "a.b.d"), @r#"!a.b.d = "0.2""#);
1173
1174 // a.b is shadowed by a.b.*
1175 let layers = [&layer1, &layer2];
1176 insta::assert_snapshot!(list(&layers, ""), @r#"
1177 !a.b = "1.0"
1178 a.c = "1.1"
1179 a.b.g = "2.0"
1180 a.b.d = "2.1"
1181 "#);
1182 insta::assert_snapshot!(list(&layers, "a.b"), @r#"
1183 !a.b = "1.0"
1184 a.b.g = "2.0"
1185 a.b.d = "2.1"
1186 "#);
1187
1188 // a.b.d is shadowed by a.b.d
1189 let layers = [&layer0, &layer2];
1190 insta::assert_snapshot!(list(&layers, ""), @r#"
1191 a.b.e = "0.0"
1192 a.b.c.f = "0.1"
1193 !a.b.d = "0.2"
1194 a.b.g = "2.0"
1195 a.b.d = "2.1"
1196 "#);
1197 insta::assert_snapshot!(list(&layers, "a.b"), @r#"
1198 a.b.e = "0.0"
1199 a.b.c.f = "0.1"
1200 !a.b.d = "0.2"
1201 a.b.g = "2.0"
1202 a.b.d = "2.1"
1203 "#);
1204 insta::assert_snapshot!(list(&layers, "a.b.c"), @r#" a.b.c.f = "0.1""#);
1205 insta::assert_snapshot!(list(&layers, "a.b.d"), @r#"
1206 !a.b.d = "0.2"
1207 a.b.d = "2.1"
1208 "#);
1209
1210 // a.b.* is shadowed by a.b, which is shadowed by a.b.*
1211 let layers = [&layer0, &layer1, &layer2];
1212 insta::assert_snapshot!(list(&layers, ""), @r#"
1213 !a.b.e = "0.0"
1214 !a.b.c.f = "0.1"
1215 !a.b.d = "0.2"
1216 !a.b = "1.0"
1217 a.c = "1.1"
1218 a.b.g = "2.0"
1219 a.b.d = "2.1"
1220 "#);
1221 insta::assert_snapshot!(list(&layers, "a.b"), @r#"
1222 !a.b.e = "0.0"
1223 !a.b.c.f = "0.1"
1224 !a.b.d = "0.2"
1225 !a.b = "1.0"
1226 a.b.g = "2.0"
1227 a.b.d = "2.1"
1228 "#);
1229 insta::assert_snapshot!(list(&layers, "a.b.c"), @r#"!a.b.c.f = "0.1""#);
1230 }
1231
1232 struct TestCase {
1233 files: &'static [&'static str],
1234 env: UnresolvedConfigEnv,
1235 wants: &'static [Want],
1236 }
1237
1238 enum Want {
1239 New(&'static str),
1240 Existing(&'static str),
1241 }
1242
1243 fn config_path_home_existing() -> TestCase {
1244 TestCase {
1245 files: &["home/.jjconfig.toml"],
1246 env: UnresolvedConfigEnv {
1247 home_dir: Some("home".into()),
1248 ..Default::default()
1249 },
1250 wants: &[Want::Existing("home/.jjconfig.toml")],
1251 }
1252 }
1253
1254 fn config_path_home_new() -> TestCase {
1255 TestCase {
1256 files: &[],
1257 env: UnresolvedConfigEnv {
1258 home_dir: Some("home".into()),
1259 ..Default::default()
1260 },
1261 wants: &[Want::New("home/.jjconfig.toml")],
1262 }
1263 }
1264
1265 fn config_path_home_existing_platform_new() -> TestCase {
1266 TestCase {
1267 files: &["home/.jjconfig.toml"],
1268 env: UnresolvedConfigEnv {
1269 home_dir: Some("home".into()),
1270 config_dir: Some("config".into()),
1271 ..Default::default()
1272 },
1273 wants: &[
1274 Want::Existing("home/.jjconfig.toml"),
1275 Want::New("config/jj/config.toml"),
1276 ],
1277 }
1278 }
1279
1280 fn config_path_platform_existing() -> TestCase {
1281 TestCase {
1282 files: &["config/jj/config.toml"],
1283 env: UnresolvedConfigEnv {
1284 home_dir: Some("home".into()),
1285 config_dir: Some("config".into()),
1286 ..Default::default()
1287 },
1288 wants: &[Want::Existing("config/jj/config.toml")],
1289 }
1290 }
1291
1292 fn config_path_platform_new() -> TestCase {
1293 TestCase {
1294 files: &[],
1295 env: UnresolvedConfigEnv {
1296 config_dir: Some("config".into()),
1297 ..Default::default()
1298 },
1299 wants: &[Want::New("config/jj/config.toml")],
1300 }
1301 }
1302
1303 fn config_path_new_prefer_platform() -> TestCase {
1304 TestCase {
1305 files: &[],
1306 env: UnresolvedConfigEnv {
1307 home_dir: Some("home".into()),
1308 config_dir: Some("config".into()),
1309 ..Default::default()
1310 },
1311 wants: &[Want::New("config/jj/config.toml")],
1312 }
1313 }
1314
1315 fn config_path_jj_config_existing() -> TestCase {
1316 TestCase {
1317 files: &["custom.toml"],
1318 env: UnresolvedConfigEnv {
1319 jj_config: Some("custom.toml".into()),
1320 ..Default::default()
1321 },
1322 wants: &[Want::Existing("custom.toml")],
1323 }
1324 }
1325
1326 fn config_path_jj_config_new() -> TestCase {
1327 TestCase {
1328 files: &[],
1329 env: UnresolvedConfigEnv {
1330 jj_config: Some("custom.toml".into()),
1331 ..Default::default()
1332 },
1333 wants: &[Want::New("custom.toml")],
1334 }
1335 }
1336
1337 fn config_path_jj_config_existing_multiple() -> TestCase {
1338 TestCase {
1339 files: &["custom1.toml", "custom2.toml"],
1340 env: UnresolvedConfigEnv {
1341 jj_config: Some(
1342 join_paths(["custom1.toml", "custom2.toml"])
1343 .unwrap()
1344 .into_string()
1345 .unwrap(),
1346 ),
1347 ..Default::default()
1348 },
1349 wants: &[
1350 Want::Existing("custom1.toml"),
1351 Want::Existing("custom2.toml"),
1352 ],
1353 }
1354 }
1355
1356 fn config_path_jj_config_new_multiple() -> TestCase {
1357 TestCase {
1358 files: &["custom1.toml"],
1359 env: UnresolvedConfigEnv {
1360 jj_config: Some(
1361 join_paths(["custom1.toml", "custom2.toml"])
1362 .unwrap()
1363 .into_string()
1364 .unwrap(),
1365 ),
1366 ..Default::default()
1367 },
1368 wants: &[Want::Existing("custom1.toml"), Want::New("custom2.toml")],
1369 }
1370 }
1371
1372 fn config_path_jj_config_empty_paths_filtered() -> TestCase {
1373 TestCase {
1374 files: &["custom1.toml"],
1375 env: UnresolvedConfigEnv {
1376 jj_config: Some(
1377 join_paths(["custom1.toml", "", "custom2.toml"])
1378 .unwrap()
1379 .into_string()
1380 .unwrap(),
1381 ),
1382 ..Default::default()
1383 },
1384 wants: &[Want::Existing("custom1.toml"), Want::New("custom2.toml")],
1385 }
1386 }
1387
1388 fn config_path_jj_config_empty() -> TestCase {
1389 TestCase {
1390 files: &[],
1391 env: UnresolvedConfigEnv {
1392 jj_config: Some("".to_owned()),
1393 ..Default::default()
1394 },
1395 wants: &[],
1396 }
1397 }
1398
1399 fn config_path_config_pick_platform() -> TestCase {
1400 TestCase {
1401 files: &["config/jj/config.toml"],
1402 env: UnresolvedConfigEnv {
1403 home_dir: Some("home".into()),
1404 config_dir: Some("config".into()),
1405 ..Default::default()
1406 },
1407 wants: &[Want::Existing("config/jj/config.toml")],
1408 }
1409 }
1410
1411 fn config_path_config_pick_home() -> TestCase {
1412 TestCase {
1413 files: &["home/.jjconfig.toml"],
1414 env: UnresolvedConfigEnv {
1415 home_dir: Some("home".into()),
1416 config_dir: Some("config".into()),
1417 ..Default::default()
1418 },
1419 wants: &[
1420 Want::Existing("home/.jjconfig.toml"),
1421 Want::New("config/jj/config.toml"),
1422 ],
1423 }
1424 }
1425
1426 fn config_path_platform_new_conf_dir_existing() -> TestCase {
1427 TestCase {
1428 files: &["config/jj/conf.d/_"],
1429 env: UnresolvedConfigEnv {
1430 home_dir: Some("home".into()),
1431 config_dir: Some("config".into()),
1432 ..Default::default()
1433 },
1434 wants: &[
1435 Want::New("config/jj/config.toml"),
1436 Want::Existing("config/jj/conf.d"),
1437 ],
1438 }
1439 }
1440
1441 fn config_path_platform_existing_conf_dir_existing() -> TestCase {
1442 TestCase {
1443 files: &["config/jj/config.toml", "config/jj/conf.d/_"],
1444 env: UnresolvedConfigEnv {
1445 home_dir: Some("home".into()),
1446 config_dir: Some("config".into()),
1447 ..Default::default()
1448 },
1449 wants: &[
1450 Want::Existing("config/jj/config.toml"),
1451 Want::Existing("config/jj/conf.d"),
1452 ],
1453 }
1454 }
1455
1456 fn config_path_all_existing() -> TestCase {
1457 TestCase {
1458 files: &[
1459 "config/jj/conf.d/_",
1460 "config/jj/config.toml",
1461 "home/.jjconfig.toml",
1462 ],
1463 env: UnresolvedConfigEnv {
1464 home_dir: Some("home".into()),
1465 config_dir: Some("config".into()),
1466 ..Default::default()
1467 },
1468 // Precedence order is important
1469 wants: &[
1470 Want::Existing("home/.jjconfig.toml"),
1471 Want::Existing("config/jj/config.toml"),
1472 Want::Existing("config/jj/conf.d"),
1473 ],
1474 }
1475 }
1476
1477 fn config_path_none() -> TestCase {
1478 TestCase {
1479 files: &[],
1480 env: Default::default(),
1481 wants: &[],
1482 }
1483 }
1484
1485 #[test_case(config_path_home_existing())]
1486 #[test_case(config_path_home_new())]
1487 #[test_case(config_path_home_existing_platform_new())]
1488 #[test_case(config_path_platform_existing())]
1489 #[test_case(config_path_platform_new())]
1490 #[test_case(config_path_new_prefer_platform())]
1491 #[test_case(config_path_jj_config_existing())]
1492 #[test_case(config_path_jj_config_new())]
1493 #[test_case(config_path_jj_config_existing_multiple())]
1494 #[test_case(config_path_jj_config_new_multiple())]
1495 #[test_case(config_path_jj_config_empty_paths_filtered())]
1496 #[test_case(config_path_jj_config_empty())]
1497 #[test_case(config_path_config_pick_platform())]
1498 #[test_case(config_path_config_pick_home())]
1499 #[test_case(config_path_platform_new_conf_dir_existing())]
1500 #[test_case(config_path_platform_existing_conf_dir_existing())]
1501 #[test_case(config_path_all_existing())]
1502 #[test_case(config_path_none())]
1503 fn test_config_path(case: TestCase) {
1504 let tmp = setup_config_fs(case.files);
1505 let env = resolve_config_env(&case.env, tmp.path());
1506
1507 let expected_existing: Vec<PathBuf> = case
1508 .wants
1509 .iter()
1510 .filter_map(|want| match want {
1511 Want::Existing(path) => Some(tmp.path().join(path)),
1512 _ => None,
1513 })
1514 .collect();
1515 assert_eq!(
1516 env.existing_user_config_paths().collect_vec(),
1517 expected_existing
1518 );
1519
1520 let expected_paths: Vec<PathBuf> = case
1521 .wants
1522 .iter()
1523 .map(|want| match want {
1524 Want::New(path) | Want::Existing(path) => tmp.path().join(path),
1525 })
1526 .collect();
1527 assert_eq!(env.user_config_paths().collect_vec(), expected_paths);
1528 }
1529
1530 fn setup_config_fs(files: &[&str]) -> tempfile::TempDir {
1531 let tmp = testutils::new_temp_dir();
1532 for file in files {
1533 let path = tmp.path().join(file);
1534 if let Some(parent) = path.parent() {
1535 std::fs::create_dir_all(parent).unwrap();
1536 }
1537 std::fs::File::create(path).unwrap();
1538 }
1539 tmp
1540 }
1541
1542 fn resolve_config_env(env: &UnresolvedConfigEnv, root: &Path) -> ConfigEnv {
1543 let home_dir = env.home_dir.as_ref().map(|p| root.join(p));
1544 let env = UnresolvedConfigEnv {
1545 config_dir: env.config_dir.as_ref().map(|p| root.join(p)),
1546 home_dir: home_dir.clone(),
1547 jj_config: env.jj_config.as_ref().map(|p| {
1548 join_paths(split_paths(p).map(|p| {
1549 if p.as_os_str().is_empty() {
1550 return p;
1551 }
1552 root.join(p)
1553 }))
1554 .unwrap()
1555 .into_string()
1556 .unwrap()
1557 }),
1558 };
1559 ConfigEnv {
1560 home_dir,
1561 repo_path: None,
1562 user_config_paths: env.resolve(),
1563 repo_config_path: None,
1564 command: None,
1565 }
1566 }
1567}