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