1use 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;
24use std::sync::Arc;
25use std::sync::LazyLock;
26use std::sync::Mutex;
27
28use etcetera::BaseStrategy as _;
29use itertools::Itertools as _;
30use jj_lib::config::ConfigFile;
31use jj_lib::config::ConfigGetError;
32use jj_lib::config::ConfigLayer;
33use jj_lib::config::ConfigLoadError;
34use jj_lib::config::ConfigMigrationRule;
35use jj_lib::config::ConfigNamePathBuf;
36use jj_lib::config::ConfigResolutionContext;
37use jj_lib::config::ConfigSource;
38use jj_lib::config::ConfigValue;
39use jj_lib::config::StackedConfig;
40use jj_lib::secure_config::LoadedSecureConfig;
41use jj_lib::secure_config::SecureConfig;
42use rand::SeedableRng as _;
43use rand_chacha::ChaCha20Rng;
44use regex::Captures;
45use regex::Regex;
46use serde::Serialize as _;
47use tracing::instrument;
48
49use crate::command_error::CommandError;
50use crate::command_error::config_error;
51use crate::command_error::config_error_with_message;
52use crate::ui::Ui;
53
54pub const CONFIG_SCHEMA: &str = include_str!("config-schema.json");
56
57const REPO_CONFIG_DIR: &str = "repos";
58const WORKSPACE_CONFIG_DIR: &str = "workspaces";
59
60pub fn parse_value_or_bare_string(value_str: &str) -> Result<ConfigValue, toml_edit::TomlError> {
63 match value_str.parse() {
64 Ok(value) => Ok(value),
65 Err(_) if is_bare_string(value_str) => Ok(value_str.into()),
66 Err(err) => Err(err),
67 }
68}
69
70fn is_bare_string(value_str: &str) -> bool {
71 let trimmed = value_str.trim_ascii().as_bytes();
74 if let (Some(&first), Some(&last)) = (trimmed.first(), trimmed.last()) {
75 !matches!(first, b'"' | b'\'' | b'[' | b'{') && !matches!(last, b'"' | b'\'' | b']' | b'}')
77 } else {
78 true }
80}
81
82pub fn to_serializable_value(value: ConfigValue) -> toml::Value {
85 match value {
86 ConfigValue::String(v) => toml::Value::String(v.into_value()),
87 ConfigValue::Integer(v) => toml::Value::Integer(v.into_value()),
88 ConfigValue::Float(v) => toml::Value::Float(v.into_value()),
89 ConfigValue::Boolean(v) => toml::Value::Boolean(v.into_value()),
90 ConfigValue::Datetime(v) => toml::Value::Datetime(v.into_value()),
91 ConfigValue::Array(array) => {
92 let array = array.into_iter().map(to_serializable_value).collect();
93 toml::Value::Array(array)
94 }
95 ConfigValue::InlineTable(table) => {
96 let table = table
97 .into_iter()
98 .map(|(k, v)| (k, to_serializable_value(v)))
99 .collect();
100 toml::Value::Table(table)
101 }
102 }
103}
104
105#[derive(Clone, Debug, serde::Serialize)]
107pub struct AnnotatedValue {
108 #[serde(serialize_with = "serialize_name")]
110 pub name: ConfigNamePathBuf,
111 #[serde(serialize_with = "serialize_value")]
113 pub value: ConfigValue,
114 #[serde(serialize_with = "serialize_source")]
116 pub source: ConfigSource,
117 pub path: Option<PathBuf>,
119 pub is_overridden: bool,
121}
122
123fn serialize_name<S>(name: &ConfigNamePathBuf, serializer: S) -> Result<S::Ok, S::Error>
124where
125 S: serde::Serializer,
126{
127 name.to_string().serialize(serializer)
128}
129
130fn serialize_value<S>(value: &ConfigValue, serializer: S) -> Result<S::Ok, S::Error>
131where
132 S: serde::Serializer,
133{
134 to_serializable_value(value.clone()).serialize(serializer)
135}
136
137fn serialize_source<S>(source: &ConfigSource, serializer: S) -> Result<S::Ok, S::Error>
138where
139 S: serde::Serializer,
140{
141 source.to_string().serialize(serializer)
142}
143
144pub fn resolved_config_values(
147 stacked_config: &StackedConfig,
148 filter_prefix: &ConfigNamePathBuf,
149) -> Vec<AnnotatedValue> {
150 let mut config_vals = vec![];
153 let mut upper_value_names = BTreeSet::new();
154 for layer in stacked_config.layers().iter().rev() {
155 let top_item = match layer.look_up_item(filter_prefix) {
156 Ok(Some(item)) => item,
157 Ok(None) => continue, Err(_) => {
159 upper_value_names.insert(filter_prefix.clone());
161 continue;
162 }
163 };
164 let mut config_stack = vec![(filter_prefix.clone(), top_item, false)];
165 while let Some((name, item, is_parent_overridden)) = config_stack.pop() {
166 if let Some(table) = item.as_table_like() {
169 let is_overridden = is_parent_overridden || upper_value_names.contains(&name);
171 for (k, v) in table.iter() {
172 let mut sub_name = name.clone();
173 sub_name.push(k);
174 config_stack.push((sub_name, v, is_overridden)); }
176 } else {
177 let maybe_child = upper_value_names
179 .range(&name..)
180 .next()
181 .filter(|next| next.starts_with(&name));
182 let is_overridden = is_parent_overridden || maybe_child.is_some();
183 if maybe_child != Some(&name) {
184 upper_value_names.insert(name.clone());
185 }
186 let value = item
187 .clone()
188 .into_value()
189 .expect("Item::None should not exist in table");
190 config_vals.push(AnnotatedValue {
191 name,
192 value,
193 source: layer.source,
194 path: layer.path.clone(),
195 is_overridden,
196 });
197 }
198 }
199 }
200 config_vals.reverse();
201 config_vals
202}
203
204#[derive(Clone, Debug)]
209pub struct RawConfig(StackedConfig);
210
211impl AsRef<StackedConfig> for RawConfig {
212 fn as_ref(&self) -> &StackedConfig {
213 &self.0
214 }
215}
216
217impl AsMut<StackedConfig> for RawConfig {
218 fn as_mut(&mut self) -> &mut StackedConfig {
219 &mut self.0
220 }
221}
222
223#[derive(Clone, Debug)]
224enum ConfigPathState {
225 New,
226 Exists,
227}
228
229#[derive(Clone, Debug)]
235struct ConfigPath {
236 path: PathBuf,
237 state: ConfigPathState,
238}
239
240impl ConfigPath {
241 fn new(path: PathBuf) -> Self {
242 use ConfigPathState::*;
243 Self {
244 state: if path.exists() { Exists } else { New },
245 path,
246 }
247 }
248
249 fn as_path(&self) -> &Path {
250 &self.path
251 }
252 fn exists(&self) -> bool {
253 match self.state {
254 ConfigPathState::Exists => true,
255 ConfigPathState::New => false,
256 }
257 }
258}
259
260fn create_dir_all(path: &Path) -> std::io::Result<()> {
263 let mut dir = std::fs::DirBuilder::new();
264 dir.recursive(true);
265 #[cfg(unix)]
266 {
267 use std::os::unix::fs::DirBuilderExt as _;
268 dir.mode(0o700);
269 }
270 dir.create(path)
271}
272
273#[derive(Clone, Default, Debug)]
275struct UnresolvedConfigEnv {
276 config_dir: Option<PathBuf>,
277 home_dir: Option<PathBuf>,
278 jj_config: Option<String>,
279}
280
281impl UnresolvedConfigEnv {
282 fn root_config_dir(&self) -> Option<PathBuf> {
283 self.config_dir.as_deref().map(|c| c.join("jj"))
284 }
285
286 fn resolve(self) -> Vec<ConfigPath> {
287 if let Some(paths) = self.jj_config {
288 return split_paths(&paths)
289 .filter(|path| !path.as_os_str().is_empty())
290 .map(ConfigPath::new)
291 .collect();
292 }
293
294 let mut paths = vec![];
295 let home_config_path = self.home_dir.map(|mut home_dir| {
296 home_dir.push(".jjconfig.toml");
297 ConfigPath::new(home_dir)
298 });
299 let platform_config_path = self.config_dir.clone().map(|mut config_dir| {
300 config_dir.push("jj");
301 config_dir.push("config.toml");
302 ConfigPath::new(config_dir)
303 });
304 let platform_config_dir = self.config_dir.map(|mut config_dir| {
305 config_dir.push("jj");
306 config_dir.push("conf.d");
307 ConfigPath::new(config_dir)
308 });
309
310 if let Some(path) = home_config_path
311 && (path.exists() || platform_config_path.is_none())
312 {
313 paths.push(path);
314 }
315
316 if let Some(path) = platform_config_path {
319 paths.push(path);
320 }
321
322 if let Some(path) = platform_config_dir
323 && path.exists()
324 {
325 paths.push(path);
326 }
327
328 paths
329 }
330}
331
332#[derive(Clone, Debug)]
333pub struct ConfigEnv {
334 home_dir: Option<PathBuf>,
335 root_config_dir: Option<PathBuf>,
336 repo_path: Option<PathBuf>,
337 workspace_path: Option<PathBuf>,
338 user_config_paths: Vec<ConfigPath>,
339 repo_config: Option<SecureConfig>,
340 workspace_config: Option<SecureConfig>,
341 command: Option<String>,
342 hostname: Option<String>,
343 rng: Arc<Mutex<ChaCha20Rng>>,
344}
345
346impl ConfigEnv {
347 pub fn from_environment() -> Self {
349 let config_dir = etcetera::choose_base_strategy()
350 .ok()
351 .map(|s| s.config_dir());
352
353 let home_dir = etcetera::home_dir()
356 .ok()
357 .map(|d| dunce::canonicalize(&d).unwrap_or(d));
358
359 let env = UnresolvedConfigEnv {
360 config_dir,
361 home_dir: home_dir.clone(),
362 jj_config: env::var("JJ_CONFIG").ok(),
363 };
364 Self {
365 home_dir,
366 root_config_dir: env.root_config_dir(),
367 repo_path: None,
368 workspace_path: None,
369 user_config_paths: env.resolve(),
370 repo_config: None,
371 workspace_config: None,
372 command: None,
373 hostname: whoami::hostname().ok(),
374 rng: Arc::new(Mutex::new(
377 if let Ok(Ok(value)) = env::var("JJ_RANDOMNESS_SEED").map(|s| s.parse::<u64>()) {
378 ChaCha20Rng::seed_from_u64(value)
379 } else {
380 ChaCha20Rng::from_os_rng()
381 },
382 )),
383 }
384 }
385
386 pub fn set_command_name(&mut self, command: String) {
387 self.command = Some(command);
388 }
389
390 fn load_secure_config(
391 &self,
392 ui: &Ui,
393 config: Option<&SecureConfig>,
394 kind: &str,
395 force: bool,
396 ) -> Result<Option<LoadedSecureConfig>, CommandError> {
397 Ok(match (config, self.root_config_dir.as_ref()) {
398 (Some(config), Some(root_config_dir)) => {
399 let mut guard = self.rng.lock().unwrap();
400 let loaded_config = if force {
401 config.load_config(&mut guard, &root_config_dir.join(kind))
402 } else {
403 config.maybe_load_config(&mut guard, &root_config_dir.join(kind))
404 }?;
405 for warning in &loaded_config.warnings {
406 writeln!(ui.warning_default(), "{warning}")?;
407 }
408 Some(loaded_config)
409 }
410 _ => None,
411 })
412 }
413
414 pub fn user_config_paths(&self) -> impl Iterator<Item = &Path> {
416 self.user_config_paths.iter().map(ConfigPath::as_path)
417 }
418
419 pub fn existing_user_config_paths(&self) -> impl Iterator<Item = &Path> {
422 self.user_config_paths
423 .iter()
424 .filter(|p| p.exists())
425 .map(ConfigPath::as_path)
426 }
427
428 pub fn user_config_files(&self, config: &RawConfig) -> Result<Vec<ConfigFile>, CommandError> {
435 config_files_for(config, ConfigSource::User, || {
436 Ok(self.new_user_config_file()?)
437 })
438 }
439
440 fn new_user_config_file(&self) -> Result<Option<ConfigFile>, ConfigLoadError> {
441 self.user_config_paths()
442 .next()
443 .map(|path| {
444 if let Some(dir) = path.parent() {
447 create_dir_all(dir).ok();
448 }
449 ConfigFile::load_or_empty(ConfigSource::User, path)
452 })
453 .transpose()
454 }
455
456 #[instrument]
459 pub fn reload_user_config(&self, config: &mut RawConfig) -> Result<(), ConfigLoadError> {
460 config.as_mut().remove_layers(ConfigSource::User);
461 for path in self.existing_user_config_paths() {
462 if path.is_dir() {
463 config.as_mut().load_dir(ConfigSource::User, path)?;
464 } else {
465 config.as_mut().load_file(ConfigSource::User, path)?;
466 }
467 }
468 Ok(())
469 }
470
471 pub fn reset_repo_path(&mut self, path: &Path) {
474 self.repo_config = Some(SecureConfig::new_repo(path.to_path_buf()));
475 self.repo_path = Some(path.to_owned());
476 }
477
478 fn maybe_repo_config_path(&self, ui: &Ui) -> Result<Option<PathBuf>, CommandError> {
480 Ok(self
481 .load_secure_config(ui, self.repo_config.as_ref(), REPO_CONFIG_DIR, false)?
482 .and_then(|c| c.config_file))
483 }
484
485 pub fn repo_config_path(&self, ui: &Ui) -> Result<Option<PathBuf>, CommandError> {
489 Ok(self
490 .load_secure_config(ui, self.repo_config.as_ref(), REPO_CONFIG_DIR, true)?
491 .and_then(|c| c.config_file))
492 }
493
494 pub fn repo_config_files(
501 &self,
502 ui: &Ui,
503 config: &RawConfig,
504 ) -> Result<Vec<ConfigFile>, CommandError> {
505 config_files_for(config, ConfigSource::Repo, || self.new_repo_config_file(ui))
506 }
507
508 fn new_repo_config_file(&self, ui: &Ui) -> Result<Option<ConfigFile>, CommandError> {
509 Ok(self
510 .repo_config_path(ui)?
511 .map(|path| ConfigFile::load_or_empty(ConfigSource::Repo, path))
514 .transpose()?)
515 }
516
517 #[instrument(skip(ui))]
520 pub fn reload_repo_config(&self, ui: &Ui, config: &mut RawConfig) -> Result<(), CommandError> {
521 config.as_mut().remove_layers(ConfigSource::Repo);
522 if let Some(path) = self.maybe_repo_config_path(ui)?
523 && path.exists()
524 {
525 config.as_mut().load_file(ConfigSource::Repo, path)?;
526 }
527 Ok(())
528 }
529
530 pub fn reset_workspace_path(&mut self, path: &Path) {
532 self.workspace_config = Some(SecureConfig::new_workspace(path.join(".jj")));
533 self.workspace_path = Some(path.to_owned());
534 }
535
536 fn maybe_workspace_config_path(&self, ui: &Ui) -> Result<Option<PathBuf>, CommandError> {
538 Ok(self
539 .load_secure_config(
540 ui,
541 self.workspace_config.as_ref(),
542 WORKSPACE_CONFIG_DIR,
543 false,
544 )?
545 .and_then(|c| c.config_file))
546 }
547
548 pub fn workspace_config_path(&self, ui: &Ui) -> Result<Option<PathBuf>, CommandError> {
552 Ok(self
553 .load_secure_config(
554 ui,
555 self.workspace_config.as_ref(),
556 WORKSPACE_CONFIG_DIR,
557 true,
558 )?
559 .and_then(|c| c.config_file))
560 }
561
562 pub fn workspace_config_files(
569 &self,
570 ui: &Ui,
571 config: &RawConfig,
572 ) -> Result<Vec<ConfigFile>, CommandError> {
573 config_files_for(config, ConfigSource::Workspace, || {
574 self.new_workspace_config_file(ui)
575 })
576 }
577
578 fn new_workspace_config_file(&self, ui: &Ui) -> Result<Option<ConfigFile>, CommandError> {
579 Ok(self
580 .workspace_config_path(ui)?
581 .map(|path| ConfigFile::load_or_empty(ConfigSource::Workspace, path))
582 .transpose()?)
583 }
584
585 #[instrument(skip(ui))]
588 pub fn reload_workspace_config(
589 &self,
590 ui: &Ui,
591 config: &mut RawConfig,
592 ) -> Result<(), CommandError> {
593 config.as_mut().remove_layers(ConfigSource::Workspace);
594 if let Some(path) = self.maybe_workspace_config_path(ui)?
595 && path.exists()
596 {
597 config.as_mut().load_file(ConfigSource::Workspace, path)?;
598 }
599 Ok(())
600 }
601
602 pub fn resolve_config(&self, config: &RawConfig) -> Result<StackedConfig, ConfigGetError> {
605 let context = ConfigResolutionContext {
606 home_dir: self.home_dir.as_deref(),
607 repo_path: self.repo_path.as_deref(),
608 workspace_path: self.workspace_path.as_deref(),
609 command: self.command.as_deref(),
610 hostname: self.hostname.as_deref().unwrap_or(""),
611 };
612 jj_lib::config::resolve(config.as_ref(), &context)
613 }
614}
615
616fn config_files_for(
617 config: &RawConfig,
618 source: ConfigSource,
619 new_file: impl FnOnce() -> Result<Option<ConfigFile>, CommandError>,
620) -> Result<Vec<ConfigFile>, CommandError> {
621 let mut files = config
622 .as_ref()
623 .layers_for(source)
624 .iter()
625 .filter_map(|layer| ConfigFile::from_layer(layer.clone()).ok())
626 .collect_vec();
627 if files.is_empty() {
628 files.extend(new_file()?);
629 }
630 Ok(files)
631}
632
633pub fn config_from_environment(default_layers: impl IntoIterator<Item = ConfigLayer>) -> RawConfig {
647 let mut config = StackedConfig::with_defaults();
648 config.extend_layers(default_layers);
649 config.add_layer(env_base_layer());
650 config.add_layer(env_overrides_layer());
651 RawConfig(config)
652}
653
654const OP_HOSTNAME: &str = "operation.hostname";
655const OP_USERNAME: &str = "operation.username";
656
657fn env_base_layer() -> ConfigLayer {
659 let mut layer = ConfigLayer::empty(ConfigSource::EnvBase);
660 if let Ok(value) =
661 whoami::hostname().inspect_err(|err| tracing::warn!(?err, "failed to get hostname"))
662 {
663 layer.set_value(OP_HOSTNAME, value).unwrap();
664 }
665 if let Ok(value) =
666 whoami::username().inspect_err(|err| tracing::warn!(?err, "failed to get username"))
667 {
668 layer.set_value(OP_USERNAME, value).unwrap();
669 } else if let Ok(value) = env::var("USER") {
670 layer.set_value(OP_USERNAME, value).unwrap();
673 }
674 if !env::var("NO_COLOR").unwrap_or_default().is_empty() {
675 layer.set_value("ui.color", "never").unwrap();
678 }
679 if let Ok(value) = env::var("VISUAL") {
680 layer.set_value("ui.editor", value).unwrap();
681 } else if let Ok(value) = env::var("EDITOR") {
682 layer.set_value("ui.editor", value).unwrap();
683 }
684 layer
687}
688
689pub fn default_config_layers() -> Vec<ConfigLayer> {
690 let parse = |text: &'static str| ConfigLayer::parse(ConfigSource::Default, text).unwrap();
693 let mut layers = vec![
694 parse(include_str!("config/colors.toml")),
695 parse(include_str!("config/hints.toml")),
696 parse(include_str!("config/merge_tools.toml")),
697 parse(include_str!("config/misc.toml")),
698 parse(include_str!("config/revsets.toml")),
699 parse(include_str!("config/templates.toml")),
700 ];
701 if cfg!(unix) {
702 layers.push(parse(include_str!("config/unix.toml")));
703 }
704 if cfg!(windows) {
705 layers.push(parse(include_str!("config/windows.toml")));
706 }
707 layers
708}
709
710fn env_overrides_layer() -> ConfigLayer {
712 let mut layer = ConfigLayer::empty(ConfigSource::EnvOverrides);
713 if let Ok(value) = env::var("JJ_USER") {
714 layer.set_value("user.name", value).unwrap();
715 }
716 if let Ok(value) = env::var("JJ_EMAIL") {
717 layer.set_value("user.email", value).unwrap();
718 }
719 if let Ok(value) = env::var("JJ_TIMESTAMP") {
720 layer.set_value("debug.commit-timestamp", value).unwrap();
721 }
722 if let Ok(Ok(value)) = env::var("JJ_RANDOMNESS_SEED").map(|s| s.parse::<i64>()) {
723 layer.set_value("debug.randomness-seed", value).unwrap();
724 }
725 if let Ok(value) = env::var("JJ_OP_TIMESTAMP") {
726 layer.set_value("debug.operation-timestamp", value).unwrap();
727 }
728 if let Ok(value) = env::var("JJ_OP_HOSTNAME") {
729 layer.set_value(OP_HOSTNAME, value).unwrap();
730 }
731 if let Ok(value) = env::var("JJ_OP_USERNAME") {
732 layer.set_value(OP_USERNAME, value).unwrap();
733 }
734 if let Ok(value) = env::var("JJ_EDITOR") {
735 layer.set_value("ui.editor", value).unwrap();
736 }
737 layer
738}
739
740#[derive(Clone, Copy, Debug, Eq, PartialEq)]
742pub enum ConfigArgKind {
743 Item,
745 File,
747}
748
749pub fn parse_config_args(
751 toml_strs: &[(ConfigArgKind, &str)],
752) -> Result<Vec<ConfigLayer>, CommandError> {
753 let source = ConfigSource::CommandArg;
754 let mut layers = Vec::new();
755 for (kind, chunk) in &toml_strs.iter().chunk_by(|&(kind, _)| kind) {
756 match kind {
757 ConfigArgKind::Item => {
758 let mut layer = ConfigLayer::empty(source);
759 for (_, item) in chunk {
760 let (name, value) = parse_config_arg_item(item)?;
761 layer.set_value(name, value).map_err(|err| {
764 config_error_with_message("--config argument cannot be set", err)
765 })?;
766 }
767 layers.push(layer);
768 }
769 ConfigArgKind::File => {
770 for (_, path) in chunk {
771 layers.push(ConfigLayer::load_from_file(source, path.into())?);
772 }
773 }
774 }
775 }
776 Ok(layers)
777}
778
779fn parse_config_arg_item(item_str: &str) -> Result<(ConfigNamePathBuf, ConfigValue), CommandError> {
781 let split_candidates = item_str.as_bytes().iter().positions(|&b| b == b'=');
783 let Some((name, value_str)) = split_candidates
784 .map(|p| (&item_str[..p], &item_str[p + 1..]))
785 .map(|(name, value)| name.parse().map(|name| (name, value)))
786 .find_or_last(Result::is_ok)
787 .transpose()
788 .map_err(|err| config_error_with_message("--config name cannot be parsed", err))?
789 else {
790 return Err(config_error("--config must be specified as NAME=VALUE"));
791 };
792 let value = parse_value_or_bare_string(value_str)
793 .map_err(|err| config_error_with_message("--config value cannot be parsed", err))?;
794 Ok((name, value))
795}
796
797pub fn default_config_migrations() -> Vec<ConfigMigrationRule> {
799 vec![
800 ConfigMigrationRule::rename_value("core.fsmonitor", "fsmonitor.backend"),
802 ConfigMigrationRule::rename_value(
804 "core.watchman.register-snapshot-trigger",
805 "fsmonitor.watchman.register-snapshot-trigger",
806 ),
807 ConfigMigrationRule::custom(
809 |layer| {
810 let Ok(Some(val)) = layer.look_up_item("git.auto-local-bookmark") else {
811 return false;
812 };
813 val.as_bool().is_some_and(|b| b)
814 },
815 |_| {
816 Ok("`git.auto-local-bookmark` is deprecated; use \
817 `remotes.<name>.auto-track-bookmarks` instead.
818Example: jj config set --user remotes.origin.auto-track-bookmarks '*'
819For details, see: https://docs.jj-vcs.dev/latest/config/#automatic-tracking-of-bookmarks"
820 .into())
821 },
822 ),
823 ConfigMigrationRule::custom(
825 |layer| {
826 let Ok(Some(val)) = layer.look_up_item("git.push-new-bookmarks") else {
827 return false;
828 };
829 val.as_bool().is_some_and(|b| b)
830 },
831 |_| {
832 Ok("`git.push-new-bookmarks` is deprecated; use \
833 `remotes.<name>.auto-track-bookmarks` instead.
834Example: jj config set --user remotes.origin.auto-track-bookmarks '*'
835For details, see: https://docs.jj-vcs.dev/latest/config/#automatic-tracking-of-bookmarks"
836 .into())
837 },
838 ),
839 ]
840}
841
842#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize)]
844#[serde(untagged)]
845pub enum CommandNameAndArgs {
846 String(String),
847 Vec(NonEmptyCommandArgsVec),
848 Structured {
849 env: HashMap<String, String>,
850 command: NonEmptyCommandArgsVec,
851 },
852}
853
854impl CommandNameAndArgs {
855 pub fn split_name(&self) -> Cow<'_, str> {
857 let (name, _) = self.split_name_and_args();
858 name
859 }
860
861 pub fn split_name_and_args(&self) -> (Cow<'_, str>, Cow<'_, [String]>) {
865 match self {
866 Self::String(s) => {
867 if s.contains('"') || s.contains('\'') {
868 let mut parts = shlex::Shlex::new(s);
869 let res = (
870 parts.next().unwrap_or_default().into(),
871 parts.by_ref().collect(),
872 );
873 if !parts.had_error {
874 return res;
875 }
876 }
877 let mut args = s.split(' ').map(|s| s.to_owned());
878 (args.next().unwrap().into(), args.collect())
879 }
880 Self::Vec(NonEmptyCommandArgsVec(a)) => (Cow::Borrowed(&a[0]), Cow::Borrowed(&a[1..])),
881 Self::Structured {
882 env: _,
883 command: cmd,
884 } => (Cow::Borrowed(&cmd.0[0]), Cow::Borrowed(&cmd.0[1..])),
885 }
886 }
887
888 pub fn as_str(&self) -> Option<&str> {
893 match self {
894 Self::String(s) => Some(s),
895 Self::Vec(_) | Self::Structured { .. } => None,
896 }
897 }
898
899 pub fn to_command(&self) -> Command {
901 let empty: HashMap<&str, &str> = HashMap::new();
902 self.to_command_with_variables(&empty)
903 }
904
905 pub fn to_command_with_variables<V: AsRef<str>>(
908 &self,
909 variables: &HashMap<&str, V>,
910 ) -> Command {
911 let (name, args) = self.split_name_and_args();
912 let mut cmd = Command::new(interpolate_variables_single(name.as_ref(), variables));
913 if let Self::Structured { env, .. } = self {
914 cmd.envs(env);
915 }
916 cmd.args(interpolate_variables(&args, variables));
917 cmd
918 }
919}
920
921impl<T: AsRef<str> + ?Sized> From<&T> for CommandNameAndArgs {
922 fn from(s: &T) -> Self {
923 Self::String(s.as_ref().to_owned())
924 }
925}
926
927impl fmt::Display for CommandNameAndArgs {
928 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
929 match self {
930 Self::String(s) => write!(f, "{s}"),
931 Self::Vec(a) => write!(f, "{}", a.0.join(" ")),
933 Self::Structured { env, command } => {
934 for (k, v) in env {
935 write!(f, "{k}={v} ")?;
936 }
937 write!(f, "{}", command.0.join(" "))
938 }
939 }
940 }
941}
942
943static VARIABLE_REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"\$([a-z0-9_]+)\b").unwrap());
945
946pub fn interpolate_variables<V: AsRef<str>>(
947 args: &[String],
948 variables: &HashMap<&str, V>,
949) -> Vec<String> {
950 args.iter()
951 .map(|arg| interpolate_variables_single(arg, variables))
952 .collect()
953}
954
955fn interpolate_variables_single<V: AsRef<str>>(arg: &str, variables: &HashMap<&str, V>) -> String {
956 VARIABLE_REGEX
957 .replace_all(arg, |caps: &Captures| {
958 let name = &caps[1];
959 if let Some(subst) = variables.get(name) {
960 subst.as_ref().to_owned()
961 } else {
962 caps[0].to_owned()
963 }
964 })
965 .into_owned()
966}
967
968pub fn find_all_variables(args: &[String]) -> impl Iterator<Item = &str> {
970 let regex = &*VARIABLE_REGEX;
971 args.iter()
972 .flat_map(|arg| regex.find_iter(arg))
973 .map(|single_match| {
974 let s = single_match.as_str();
975 &s[1..]
976 })
977}
978
979#[derive(Clone, Debug, Eq, Hash, PartialEq, serde::Deserialize)]
982#[serde(try_from = "Vec<String>")]
983pub struct NonEmptyCommandArgsVec(Vec<String>);
984
985impl TryFrom<Vec<String>> for NonEmptyCommandArgsVec {
986 type Error = &'static str;
987
988 fn try_from(args: Vec<String>) -> Result<Self, Self::Error> {
989 if args.is_empty() {
990 Err("command arguments should not be empty")
991 } else {
992 Ok(Self(args))
993 }
994 }
995}
996
997#[cfg(test)]
998mod tests {
999 use std::env::join_paths;
1000 use std::fmt::Write as _;
1001
1002 use indoc::indoc;
1003 use maplit::hashmap;
1004 use test_case::test_case;
1005
1006 use super::*;
1007
1008 fn insta_settings() -> insta::Settings {
1009 let mut settings = insta::Settings::clone_current();
1010 settings.add_filter(r"\bDecor \{[^}]*\}", "Decor { .. }");
1012 settings
1013 }
1014
1015 #[test]
1016 fn test_parse_value_or_bare_string() {
1017 let parse = |s: &str| parse_value_or_bare_string(s);
1018
1019 assert_eq!(parse("true").unwrap().as_bool(), Some(true));
1021 assert_eq!(parse("42").unwrap().as_integer(), Some(42));
1022 assert_eq!(parse("-1").unwrap().as_integer(), Some(-1));
1023 assert_eq!(parse("'a'").unwrap().as_str(), Some("a"));
1024 assert!(parse("[]").unwrap().is_array());
1025 assert!(parse("{ a = 'b' }").unwrap().is_inline_table());
1026
1027 assert_eq!(parse("").unwrap().as_str(), Some(""));
1029 assert_eq!(parse("John Doe").unwrap().as_str(), Some("John Doe"));
1030 assert_eq!(parse("Doe, John").unwrap().as_str(), Some("Doe, John"));
1031 assert_eq!(parse("It's okay").unwrap().as_str(), Some("It's okay"));
1032 assert_eq!(
1033 parse("<foo+bar@example.org>").unwrap().as_str(),
1034 Some("<foo+bar@example.org>")
1035 );
1036 assert_eq!(parse("#ff00aa").unwrap().as_str(), Some("#ff00aa"));
1037 assert_eq!(parse("all()").unwrap().as_str(), Some("all()"));
1038 assert_eq!(parse("glob:*.*").unwrap().as_str(), Some("glob:*.*"));
1039 assert_eq!(parse("柔術").unwrap().as_str(), Some("柔術"));
1040
1041 assert!(parse("'foo").is_err());
1043 assert!(parse(r#" bar" "#).is_err());
1044 assert!(parse("[0 1]").is_err());
1045 assert!(parse("{ x = y }").is_err());
1046 assert!(parse("\n { x").is_err());
1047 assert!(parse(" x ] ").is_err());
1048 assert!(parse("[table]\nkey = 'value'").is_err());
1049 }
1050
1051 #[test]
1052 fn test_parse_config_arg_item() {
1053 assert!(parse_config_arg_item("").is_err());
1054 assert!(parse_config_arg_item("a").is_err());
1055 assert!(parse_config_arg_item("=").is_err());
1056 assert!(parse_config_arg_item("a = 'b'").is_err());
1059
1060 let (name, value) = parse_config_arg_item("a=b").unwrap();
1061 assert_eq!(name, ConfigNamePathBuf::from_iter(["a"]));
1062 assert_eq!(value.as_str(), Some("b"));
1063
1064 let (name, value) = parse_config_arg_item("a=").unwrap();
1065 assert_eq!(name, ConfigNamePathBuf::from_iter(["a"]));
1066 assert_eq!(value.as_str(), Some(""));
1067
1068 let (name, value) = parse_config_arg_item("a= ").unwrap();
1069 assert_eq!(name, ConfigNamePathBuf::from_iter(["a"]));
1070 assert_eq!(value.as_str(), Some(" "));
1071
1072 let (name, value) = parse_config_arg_item("a=b=c").unwrap();
1074 assert_eq!(name, ConfigNamePathBuf::from_iter(["a"]));
1075 assert_eq!(value.as_str(), Some("b=c"));
1076
1077 let (name, value) = parse_config_arg_item("a.b=true").unwrap();
1078 assert_eq!(name, ConfigNamePathBuf::from_iter(["a", "b"]));
1079 assert_eq!(value.as_bool(), Some(true));
1080
1081 let (name, value) = parse_config_arg_item("a='b=c'").unwrap();
1082 assert_eq!(name, ConfigNamePathBuf::from_iter(["a"]));
1083 assert_eq!(value.as_str(), Some("b=c"));
1084
1085 let (name, value) = parse_config_arg_item("'a=b'=c").unwrap();
1086 assert_eq!(name, ConfigNamePathBuf::from_iter(["a=b"]));
1087 assert_eq!(value.as_str(), Some("c"));
1088
1089 let (name, value) = parse_config_arg_item("'a = b=c '={d = 'e=f'}").unwrap();
1090 assert_eq!(name, ConfigNamePathBuf::from_iter(["a = b=c "]));
1091 assert!(value.is_inline_table());
1092 assert_eq!(value.to_string(), "{d = 'e=f'}");
1093 }
1094
1095 #[test]
1096 fn test_command_args() {
1097 let mut config = StackedConfig::empty();
1098 config.add_layer(
1099 ConfigLayer::parse(
1100 ConfigSource::User,
1101 indoc! {"
1102 empty_array = []
1103 empty_string = ''
1104 array = ['emacs', '-nw']
1105 string = 'emacs -nw'
1106 string_quoted = '\"spaced path/to/emacs\" -nw'
1107 structured.env = { KEY1 = 'value1', KEY2 = 'value2' }
1108 structured.command = ['emacs', '-nw']
1109 "},
1110 )
1111 .unwrap(),
1112 );
1113
1114 assert!(config.get::<CommandNameAndArgs>("empty_array").is_err());
1115
1116 let command_args: CommandNameAndArgs = config.get("empty_string").unwrap();
1117 assert_eq!(command_args, CommandNameAndArgs::String("".to_owned()));
1118 let (name, args) = command_args.split_name_and_args();
1119 assert_eq!(name, "");
1120 assert!(args.is_empty());
1121
1122 let command_args: CommandNameAndArgs = config.get("array").unwrap();
1123 assert_eq!(
1124 command_args,
1125 CommandNameAndArgs::Vec(NonEmptyCommandArgsVec(
1126 ["emacs", "-nw",].map(|s| s.to_owned()).to_vec()
1127 ))
1128 );
1129 let (name, args) = command_args.split_name_and_args();
1130 assert_eq!(name, "emacs");
1131 assert_eq!(args, ["-nw"].as_ref());
1132
1133 let command_args: CommandNameAndArgs = config.get("string").unwrap();
1134 assert_eq!(
1135 command_args,
1136 CommandNameAndArgs::String("emacs -nw".to_owned())
1137 );
1138 let (name, args) = command_args.split_name_and_args();
1139 assert_eq!(name, "emacs");
1140 assert_eq!(args, ["-nw"].as_ref());
1141
1142 let command_args: CommandNameAndArgs = config.get("string_quoted").unwrap();
1143 assert_eq!(
1144 command_args,
1145 CommandNameAndArgs::String("\"spaced path/to/emacs\" -nw".to_owned())
1146 );
1147 let (name, args) = command_args.split_name_and_args();
1148 assert_eq!(name, "spaced path/to/emacs");
1149 assert_eq!(args, ["-nw"].as_ref());
1150
1151 let command_args: CommandNameAndArgs = config.get("structured").unwrap();
1152 assert_eq!(
1153 command_args,
1154 CommandNameAndArgs::Structured {
1155 env: hashmap! {
1156 "KEY1".to_string() => "value1".to_string(),
1157 "KEY2".to_string() => "value2".to_string(),
1158 },
1159 command: NonEmptyCommandArgsVec(["emacs", "-nw",].map(|s| s.to_owned()).to_vec())
1160 }
1161 );
1162 let (name, args) = command_args.split_name_and_args();
1163 assert_eq!(name, "emacs");
1164 assert_eq!(args, ["-nw"].as_ref());
1165 }
1166
1167 #[test]
1168 fn test_resolved_config_values_empty() {
1169 let config = StackedConfig::empty();
1170 assert!(resolved_config_values(&config, &ConfigNamePathBuf::root()).is_empty());
1171 }
1172
1173 #[test]
1174 fn test_resolved_config_values_single_key() {
1175 let settings = insta_settings();
1176 let _guard = settings.bind_to_scope();
1177 let mut env_base_layer = ConfigLayer::empty(ConfigSource::EnvBase);
1178 env_base_layer
1179 .set_value("user.name", "base-user-name")
1180 .unwrap();
1181 env_base_layer
1182 .set_value("user.email", "base@user.email")
1183 .unwrap();
1184 let mut repo_layer = ConfigLayer::empty(ConfigSource::Repo);
1185 repo_layer
1186 .set_value("user.email", "repo@user.email")
1187 .unwrap();
1188 let mut config = StackedConfig::empty();
1189 config.add_layer(env_base_layer);
1190 config.add_layer(repo_layer);
1191 insta::assert_debug_snapshot!(
1193 resolved_config_values(&config, &ConfigNamePathBuf::root()),
1194 @r#"
1195 [
1196 AnnotatedValue {
1197 name: ConfigNamePathBuf(
1198 [
1199 Key {
1200 key: "user",
1201 repr: None,
1202 leaf_decor: Decor { .. },
1203 dotted_decor: Decor { .. },
1204 },
1205 Key {
1206 key: "name",
1207 repr: None,
1208 leaf_decor: Decor { .. },
1209 dotted_decor: Decor { .. },
1210 },
1211 ],
1212 ),
1213 value: String(
1214 Formatted {
1215 value: "base-user-name",
1216 repr: "default",
1217 decor: Decor { .. },
1218 },
1219 ),
1220 source: EnvBase,
1221 path: None,
1222 is_overridden: false,
1223 },
1224 AnnotatedValue {
1225 name: ConfigNamePathBuf(
1226 [
1227 Key {
1228 key: "user",
1229 repr: None,
1230 leaf_decor: Decor { .. },
1231 dotted_decor: Decor { .. },
1232 },
1233 Key {
1234 key: "email",
1235 repr: None,
1236 leaf_decor: Decor { .. },
1237 dotted_decor: Decor { .. },
1238 },
1239 ],
1240 ),
1241 value: String(
1242 Formatted {
1243 value: "base@user.email",
1244 repr: "default",
1245 decor: Decor { .. },
1246 },
1247 ),
1248 source: EnvBase,
1249 path: None,
1250 is_overridden: true,
1251 },
1252 AnnotatedValue {
1253 name: ConfigNamePathBuf(
1254 [
1255 Key {
1256 key: "user",
1257 repr: None,
1258 leaf_decor: Decor { .. },
1259 dotted_decor: Decor { .. },
1260 },
1261 Key {
1262 key: "email",
1263 repr: None,
1264 leaf_decor: Decor { .. },
1265 dotted_decor: Decor { .. },
1266 },
1267 ],
1268 ),
1269 value: String(
1270 Formatted {
1271 value: "repo@user.email",
1272 repr: "default",
1273 decor: Decor { .. },
1274 },
1275 ),
1276 source: Repo,
1277 path: None,
1278 is_overridden: false,
1279 },
1280 ]
1281 "#
1282 );
1283 }
1284
1285 #[test]
1286 fn test_resolved_config_values_filter_path() {
1287 let settings = insta_settings();
1288 let _guard = settings.bind_to_scope();
1289 let mut user_layer = ConfigLayer::empty(ConfigSource::User);
1290 user_layer.set_value("test-table1.foo", "user-FOO").unwrap();
1291 user_layer.set_value("test-table2.bar", "user-BAR").unwrap();
1292 let mut repo_layer = ConfigLayer::empty(ConfigSource::Repo);
1293 repo_layer.set_value("test-table1.bar", "repo-BAR").unwrap();
1294 let mut config = StackedConfig::empty();
1295 config.add_layer(user_layer);
1296 config.add_layer(repo_layer);
1297 insta::assert_debug_snapshot!(
1298 resolved_config_values(&config, &ConfigNamePathBuf::from_iter(["test-table1"])),
1299 @r#"
1300 [
1301 AnnotatedValue {
1302 name: ConfigNamePathBuf(
1303 [
1304 Key {
1305 key: "test-table1",
1306 repr: None,
1307 leaf_decor: Decor { .. },
1308 dotted_decor: Decor { .. },
1309 },
1310 Key {
1311 key: "foo",
1312 repr: None,
1313 leaf_decor: Decor { .. },
1314 dotted_decor: Decor { .. },
1315 },
1316 ],
1317 ),
1318 value: String(
1319 Formatted {
1320 value: "user-FOO",
1321 repr: "default",
1322 decor: Decor { .. },
1323 },
1324 ),
1325 source: User,
1326 path: None,
1327 is_overridden: false,
1328 },
1329 AnnotatedValue {
1330 name: ConfigNamePathBuf(
1331 [
1332 Key {
1333 key: "test-table1",
1334 repr: None,
1335 leaf_decor: Decor { .. },
1336 dotted_decor: Decor { .. },
1337 },
1338 Key {
1339 key: "bar",
1340 repr: None,
1341 leaf_decor: Decor { .. },
1342 dotted_decor: Decor { .. },
1343 },
1344 ],
1345 ),
1346 value: String(
1347 Formatted {
1348 value: "repo-BAR",
1349 repr: "default",
1350 decor: Decor { .. },
1351 },
1352 ),
1353 source: Repo,
1354 path: None,
1355 is_overridden: false,
1356 },
1357 ]
1358 "#
1359 );
1360 }
1361
1362 #[test]
1363 fn test_resolved_config_values_overridden() {
1364 let list = |layers: &[&ConfigLayer], prefix: &str| -> String {
1365 let mut config = StackedConfig::empty();
1366 config.extend_layers(layers.iter().copied().cloned());
1367 let prefix = if prefix.is_empty() {
1368 ConfigNamePathBuf::root()
1369 } else {
1370 prefix.parse().unwrap()
1371 };
1372 let mut output = String::new();
1373 for annotated in resolved_config_values(&config, &prefix) {
1374 let AnnotatedValue { name, value, .. } = &annotated;
1375 let sigil = if annotated.is_overridden { '!' } else { ' ' };
1376 writeln!(output, "{sigil}{name} = {value}").unwrap();
1377 }
1378 output
1379 };
1380
1381 let mut layer0 = ConfigLayer::empty(ConfigSource::User);
1382 layer0.set_value("a.b.e", "0.0").unwrap();
1383 layer0.set_value("a.b.c.f", "0.1").unwrap();
1384 layer0.set_value("a.b.d", "0.2").unwrap();
1385 let mut layer1 = ConfigLayer::empty(ConfigSource::User);
1386 layer1.set_value("a.b", "1.0").unwrap();
1387 layer1.set_value("a.c", "1.1").unwrap();
1388 let mut layer2 = ConfigLayer::empty(ConfigSource::User);
1389 layer2.set_value("a.b.g", "2.0").unwrap();
1390 layer2.set_value("a.b.d", "2.1").unwrap();
1391
1392 let layers = [&layer0, &layer1];
1394 insta::assert_snapshot!(list(&layers, ""), @r#"
1395 !a.b.e = "0.0"
1396 !a.b.c.f = "0.1"
1397 !a.b.d = "0.2"
1398 a.b = "1.0"
1399 a.c = "1.1"
1400 "#);
1401 insta::assert_snapshot!(list(&layers, "a.b"), @r#"
1402 !a.b.e = "0.0"
1403 !a.b.c.f = "0.1"
1404 !a.b.d = "0.2"
1405 a.b = "1.0"
1406 "#);
1407 insta::assert_snapshot!(list(&layers, "a.b.c"), @r#"!a.b.c.f = "0.1""#);
1408 insta::assert_snapshot!(list(&layers, "a.b.d"), @r#"!a.b.d = "0.2""#);
1409
1410 let layers = [&layer1, &layer2];
1412 insta::assert_snapshot!(list(&layers, ""), @r#"
1413 !a.b = "1.0"
1414 a.c = "1.1"
1415 a.b.g = "2.0"
1416 a.b.d = "2.1"
1417 "#);
1418 insta::assert_snapshot!(list(&layers, "a.b"), @r#"
1419 !a.b = "1.0"
1420 a.b.g = "2.0"
1421 a.b.d = "2.1"
1422 "#);
1423
1424 let layers = [&layer0, &layer2];
1426 insta::assert_snapshot!(list(&layers, ""), @r#"
1427 a.b.e = "0.0"
1428 a.b.c.f = "0.1"
1429 !a.b.d = "0.2"
1430 a.b.g = "2.0"
1431 a.b.d = "2.1"
1432 "#);
1433 insta::assert_snapshot!(list(&layers, "a.b"), @r#"
1434 a.b.e = "0.0"
1435 a.b.c.f = "0.1"
1436 !a.b.d = "0.2"
1437 a.b.g = "2.0"
1438 a.b.d = "2.1"
1439 "#);
1440 insta::assert_snapshot!(list(&layers, "a.b.c"), @r#" a.b.c.f = "0.1""#);
1441 insta::assert_snapshot!(list(&layers, "a.b.d"), @r#"
1442 !a.b.d = "0.2"
1443 a.b.d = "2.1"
1444 "#);
1445
1446 let layers = [&layer0, &layer1, &layer2];
1448 insta::assert_snapshot!(list(&layers, ""), @r#"
1449 !a.b.e = "0.0"
1450 !a.b.c.f = "0.1"
1451 !a.b.d = "0.2"
1452 !a.b = "1.0"
1453 a.c = "1.1"
1454 a.b.g = "2.0"
1455 a.b.d = "2.1"
1456 "#);
1457 insta::assert_snapshot!(list(&layers, "a.b"), @r#"
1458 !a.b.e = "0.0"
1459 !a.b.c.f = "0.1"
1460 !a.b.d = "0.2"
1461 !a.b = "1.0"
1462 a.b.g = "2.0"
1463 a.b.d = "2.1"
1464 "#);
1465 insta::assert_snapshot!(list(&layers, "a.b.c"), @r#"!a.b.c.f = "0.1""#);
1466 }
1467
1468 struct TestCase {
1469 files: &'static [&'static str],
1470 env: UnresolvedConfigEnv,
1471 wants: Vec<Want>,
1472 }
1473
1474 #[derive(Debug)]
1475 enum WantState {
1476 New,
1477 Existing,
1478 }
1479 #[derive(Debug)]
1480 struct Want {
1481 path: &'static str,
1482 state: WantState,
1483 }
1484
1485 impl Want {
1486 const fn new(path: &'static str) -> Self {
1487 Self {
1488 path,
1489 state: WantState::New,
1490 }
1491 }
1492
1493 const fn existing(path: &'static str) -> Self {
1494 Self {
1495 path,
1496 state: WantState::Existing,
1497 }
1498 }
1499
1500 fn rooted_path(&self, root: &Path) -> PathBuf {
1501 root.join(self.path)
1502 }
1503
1504 fn exists(&self) -> bool {
1505 matches!(self.state, WantState::Existing)
1506 }
1507 }
1508
1509 fn config_path_home_existing() -> TestCase {
1510 TestCase {
1511 files: &["home/.jjconfig.toml"],
1512 env: UnresolvedConfigEnv {
1513 home_dir: Some("home".into()),
1514 ..Default::default()
1515 },
1516 wants: vec![Want::existing("home/.jjconfig.toml")],
1517 }
1518 }
1519
1520 fn config_path_home_new() -> TestCase {
1521 TestCase {
1522 files: &[],
1523 env: UnresolvedConfigEnv {
1524 home_dir: Some("home".into()),
1525 ..Default::default()
1526 },
1527 wants: vec![Want::new("home/.jjconfig.toml")],
1528 }
1529 }
1530
1531 fn config_path_home_existing_platform_new() -> TestCase {
1532 TestCase {
1533 files: &["home/.jjconfig.toml"],
1534 env: UnresolvedConfigEnv {
1535 home_dir: Some("home".into()),
1536 config_dir: Some("config".into()),
1537 ..Default::default()
1538 },
1539 wants: vec![
1540 Want::existing("home/.jjconfig.toml"),
1541 Want::new("config/jj/config.toml"),
1542 ],
1543 }
1544 }
1545
1546 fn config_path_platform_existing() -> TestCase {
1547 TestCase {
1548 files: &["config/jj/config.toml"],
1549 env: UnresolvedConfigEnv {
1550 home_dir: Some("home".into()),
1551 config_dir: Some("config".into()),
1552 ..Default::default()
1553 },
1554 wants: vec![Want::existing("config/jj/config.toml")],
1555 }
1556 }
1557
1558 fn config_path_platform_new() -> TestCase {
1559 TestCase {
1560 files: &[],
1561 env: UnresolvedConfigEnv {
1562 config_dir: Some("config".into()),
1563 ..Default::default()
1564 },
1565 wants: vec![Want::new("config/jj/config.toml")],
1566 }
1567 }
1568
1569 fn config_path_new_prefer_platform() -> TestCase {
1570 TestCase {
1571 files: &[],
1572 env: UnresolvedConfigEnv {
1573 home_dir: Some("home".into()),
1574 config_dir: Some("config".into()),
1575 ..Default::default()
1576 },
1577 wants: vec![Want::new("config/jj/config.toml")],
1578 }
1579 }
1580
1581 fn config_path_jj_config_existing() -> TestCase {
1582 TestCase {
1583 files: &["custom.toml"],
1584 env: UnresolvedConfigEnv {
1585 jj_config: Some("custom.toml".into()),
1586 ..Default::default()
1587 },
1588 wants: vec![Want::existing("custom.toml")],
1589 }
1590 }
1591
1592 fn config_path_jj_config_new() -> TestCase {
1593 TestCase {
1594 files: &[],
1595 env: UnresolvedConfigEnv {
1596 jj_config: Some("custom.toml".into()),
1597 ..Default::default()
1598 },
1599 wants: vec![Want::new("custom.toml")],
1600 }
1601 }
1602
1603 fn config_path_jj_config_existing_multiple() -> TestCase {
1604 TestCase {
1605 files: &["custom1.toml", "custom2.toml"],
1606 env: UnresolvedConfigEnv {
1607 jj_config: Some(
1608 join_paths(["custom1.toml", "custom2.toml"])
1609 .unwrap()
1610 .into_string()
1611 .unwrap(),
1612 ),
1613 ..Default::default()
1614 },
1615 wants: vec![
1616 Want::existing("custom1.toml"),
1617 Want::existing("custom2.toml"),
1618 ],
1619 }
1620 }
1621
1622 fn config_path_jj_config_new_multiple() -> TestCase {
1623 TestCase {
1624 files: &["custom1.toml"],
1625 env: UnresolvedConfigEnv {
1626 jj_config: Some(
1627 join_paths(["custom1.toml", "custom2.toml"])
1628 .unwrap()
1629 .into_string()
1630 .unwrap(),
1631 ),
1632 ..Default::default()
1633 },
1634 wants: vec![Want::existing("custom1.toml"), Want::new("custom2.toml")],
1635 }
1636 }
1637
1638 fn config_path_jj_config_empty_paths_filtered() -> TestCase {
1639 TestCase {
1640 files: &["custom1.toml"],
1641 env: UnresolvedConfigEnv {
1642 jj_config: Some(
1643 join_paths(["custom1.toml", "", "custom2.toml"])
1644 .unwrap()
1645 .into_string()
1646 .unwrap(),
1647 ),
1648 ..Default::default()
1649 },
1650 wants: vec![Want::existing("custom1.toml"), Want::new("custom2.toml")],
1651 }
1652 }
1653
1654 fn config_path_jj_config_empty() -> TestCase {
1655 TestCase {
1656 files: &[],
1657 env: UnresolvedConfigEnv {
1658 jj_config: Some("".to_owned()),
1659 ..Default::default()
1660 },
1661 wants: vec![],
1662 }
1663 }
1664
1665 fn config_path_config_pick_platform() -> TestCase {
1666 TestCase {
1667 files: &["config/jj/config.toml"],
1668 env: UnresolvedConfigEnv {
1669 home_dir: Some("home".into()),
1670 config_dir: Some("config".into()),
1671 ..Default::default()
1672 },
1673 wants: vec![Want::existing("config/jj/config.toml")],
1674 }
1675 }
1676
1677 fn config_path_config_pick_home() -> TestCase {
1678 TestCase {
1679 files: &["home/.jjconfig.toml"],
1680 env: UnresolvedConfigEnv {
1681 home_dir: Some("home".into()),
1682 config_dir: Some("config".into()),
1683 ..Default::default()
1684 },
1685 wants: vec![
1686 Want::existing("home/.jjconfig.toml"),
1687 Want::new("config/jj/config.toml"),
1688 ],
1689 }
1690 }
1691
1692 fn config_path_platform_new_conf_dir_existing() -> TestCase {
1693 TestCase {
1694 files: &["config/jj/conf.d/_"],
1695 env: UnresolvedConfigEnv {
1696 home_dir: Some("home".into()),
1697 config_dir: Some("config".into()),
1698 ..Default::default()
1699 },
1700 wants: vec![
1701 Want::new("config/jj/config.toml"),
1702 Want::existing("config/jj/conf.d"),
1703 ],
1704 }
1705 }
1706
1707 fn config_path_platform_existing_conf_dir_existing() -> TestCase {
1708 TestCase {
1709 files: &["config/jj/config.toml", "config/jj/conf.d/_"],
1710 env: UnresolvedConfigEnv {
1711 home_dir: Some("home".into()),
1712 config_dir: Some("config".into()),
1713 ..Default::default()
1714 },
1715 wants: vec![
1716 Want::existing("config/jj/config.toml"),
1717 Want::existing("config/jj/conf.d"),
1718 ],
1719 }
1720 }
1721
1722 fn config_path_all_existing() -> TestCase {
1723 TestCase {
1724 files: &[
1725 "config/jj/conf.d/_",
1726 "config/jj/config.toml",
1727 "home/.jjconfig.toml",
1728 ],
1729 env: UnresolvedConfigEnv {
1730 home_dir: Some("home".into()),
1731 config_dir: Some("config".into()),
1732 ..Default::default()
1733 },
1734 wants: vec![
1736 Want::existing("home/.jjconfig.toml"),
1737 Want::existing("config/jj/config.toml"),
1738 Want::existing("config/jj/conf.d"),
1739 ],
1740 }
1741 }
1742
1743 fn config_path_none() -> TestCase {
1744 TestCase {
1745 files: &[],
1746 env: Default::default(),
1747 wants: vec![],
1748 }
1749 }
1750
1751 #[test_case(config_path_home_existing())]
1752 #[test_case(config_path_home_new())]
1753 #[test_case(config_path_home_existing_platform_new())]
1754 #[test_case(config_path_platform_existing())]
1755 #[test_case(config_path_platform_new())]
1756 #[test_case(config_path_new_prefer_platform())]
1757 #[test_case(config_path_jj_config_existing())]
1758 #[test_case(config_path_jj_config_new())]
1759 #[test_case(config_path_jj_config_existing_multiple())]
1760 #[test_case(config_path_jj_config_new_multiple())]
1761 #[test_case(config_path_jj_config_empty_paths_filtered())]
1762 #[test_case(config_path_jj_config_empty())]
1763 #[test_case(config_path_config_pick_platform())]
1764 #[test_case(config_path_config_pick_home())]
1765 #[test_case(config_path_platform_new_conf_dir_existing())]
1766 #[test_case(config_path_platform_existing_conf_dir_existing())]
1767 #[test_case(config_path_all_existing())]
1768 #[test_case(config_path_none())]
1769 fn test_config_path(case: TestCase) {
1770 let tmp = setup_config_fs(case.files);
1771 let env = resolve_config_env(&case.env, tmp.path());
1772
1773 let all_expected_paths = case
1774 .wants
1775 .iter()
1776 .map(|w| w.rooted_path(tmp.path()))
1777 .collect_vec();
1778 let exists_expected_paths = case
1779 .wants
1780 .iter()
1781 .filter(|w| w.exists())
1782 .map(|w| w.rooted_path(tmp.path()))
1783 .collect_vec();
1784
1785 let all_paths = env.user_config_paths().collect_vec();
1786 let exists_paths = env.existing_user_config_paths().collect_vec();
1787
1788 assert_eq!(all_paths, all_expected_paths);
1789 assert_eq!(exists_paths, exists_expected_paths);
1790 }
1791
1792 fn setup_config_fs(files: &[&str]) -> tempfile::TempDir {
1793 let tmp = testutils::new_temp_dir();
1794 for file in files {
1795 let path = tmp.path().join(file);
1796 if let Some(parent) = path.parent() {
1797 std::fs::create_dir_all(parent).unwrap();
1798 }
1799 std::fs::File::create(path).unwrap();
1800 }
1801 tmp
1802 }
1803
1804 fn resolve_config_env(env: &UnresolvedConfigEnv, root: &Path) -> ConfigEnv {
1805 let home_dir = env.home_dir.as_ref().map(|p| root.join(p));
1806 let env = UnresolvedConfigEnv {
1807 config_dir: env.config_dir.as_ref().map(|p| root.join(p)),
1808 home_dir: home_dir.clone(),
1809 jj_config: env.jj_config.as_ref().map(|p| {
1810 join_paths(split_paths(p).map(|p| {
1811 if p.as_os_str().is_empty() {
1812 return p;
1813 }
1814 root.join(p)
1815 }))
1816 .unwrap()
1817 .into_string()
1818 .unwrap()
1819 }),
1820 };
1821 ConfigEnv {
1822 home_dir,
1823 root_config_dir: None,
1824 repo_path: None,
1825 workspace_path: None,
1826 user_config_paths: env.resolve(),
1827 repo_config: None,
1828 workspace_config: None,
1829 command: None,
1830 hostname: None,
1831 rng: Arc::new(Mutex::new(ChaCha20Rng::seed_from_u64(0))),
1832 }
1833 }
1834}