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
629fn config_files_for(
630 config: &RawConfig,
631 source: ConfigSource,
632 new_file: impl FnOnce() -> Result<Option<ConfigFile>, CommandError>,
633) -> Result<Vec<ConfigFile>, CommandError> {
634 let mut files = config
635 .as_ref()
636 .layers_for(source)
637 .iter()
638 .filter_map(|layer| ConfigFile::from_layer(layer.clone()).ok())
639 .collect_vec();
640 if files.is_empty() {
641 files.extend(new_file()?);
642 }
643 Ok(files)
644}
645
646pub fn config_from_environment(default_layers: impl IntoIterator<Item = ConfigLayer>) -> RawConfig {
660 let mut config = StackedConfig::with_defaults();
661 config.extend_layers(default_layers);
662 config.add_layer(env_base_layer());
663 config.add_layer(env_overrides_layer());
664 RawConfig(config)
665}
666
667const OP_HOSTNAME: &str = "operation.hostname";
668const OP_USERNAME: &str = "operation.username";
669
670fn env_base_layer() -> ConfigLayer {
672 let mut layer = ConfigLayer::empty(ConfigSource::EnvBase);
673 if let Ok(value) =
674 whoami::hostname().inspect_err(|err| tracing::warn!(?err, "failed to get hostname"))
675 {
676 layer.set_value(OP_HOSTNAME, value).unwrap();
677 }
678 if let Ok(value) =
679 whoami::username().inspect_err(|err| tracing::warn!(?err, "failed to get username"))
680 {
681 layer.set_value(OP_USERNAME, value).unwrap();
682 } else if let Ok(value) = env::var("USER") {
683 layer.set_value(OP_USERNAME, value).unwrap();
686 }
687 if !env::var("NO_COLOR").unwrap_or_default().is_empty() {
688 layer.set_value("ui.color", "never").unwrap();
691 }
692 if let Ok(value) = env::var("VISUAL") {
693 layer.set_value("ui.editor", value).unwrap();
694 } else if let Ok(value) = env::var("EDITOR") {
695 layer.set_value("ui.editor", value).unwrap();
696 }
697 layer
700}
701
702pub fn default_config_layers() -> Vec<ConfigLayer> {
703 let parse = |text: &'static str| ConfigLayer::parse(ConfigSource::Default, text).unwrap();
706 let mut layers = vec![
707 parse(include_str!("config/colors.toml")),
708 parse(include_str!("config/hints.toml")),
709 parse(include_str!("config/merge_tools.toml")),
710 parse(include_str!("config/misc.toml")),
711 parse(include_str!("config/revsets.toml")),
712 parse(include_str!("config/templates.toml")),
713 ];
714 if cfg!(unix) {
715 layers.push(parse(include_str!("config/unix.toml")));
716 }
717 if cfg!(windows) {
718 layers.push(parse(include_str!("config/windows.toml")));
719 }
720 layers
721}
722
723fn env_overrides_layer() -> ConfigLayer {
725 let mut layer = ConfigLayer::empty(ConfigSource::EnvOverrides);
726 if let Ok(value) = env::var("JJ_USER") {
727 layer.set_value("user.name", value).unwrap();
728 }
729 if let Ok(value) = env::var("JJ_EMAIL") {
730 layer.set_value("user.email", value).unwrap();
731 }
732 if let Ok(value) = env::var("JJ_TIMESTAMP") {
733 layer.set_value("debug.commit-timestamp", value).unwrap();
734 }
735 if let Ok(Ok(value)) = env::var("JJ_RANDOMNESS_SEED").map(|s| s.parse::<i64>()) {
736 layer.set_value("debug.randomness-seed", value).unwrap();
737 }
738 if let Ok(value) = env::var("JJ_OP_TIMESTAMP") {
739 layer.set_value("debug.operation-timestamp", value).unwrap();
740 }
741 if let Ok(value) = env::var("JJ_OP_HOSTNAME") {
742 layer.set_value(OP_HOSTNAME, value).unwrap();
743 }
744 if let Ok(value) = env::var("JJ_OP_USERNAME") {
745 layer.set_value(OP_USERNAME, value).unwrap();
746 }
747 if let Ok(value) = env::var("JJ_EDITOR") {
748 layer.set_value("ui.editor", value).unwrap();
749 }
750 layer
751}
752
753#[derive(Clone, Copy, Debug, Eq, PartialEq)]
755pub enum ConfigArgKind {
756 Item,
758 File,
760}
761
762pub fn parse_config_args(
764 toml_strs: &[(ConfigArgKind, &str)],
765) -> Result<Vec<ConfigLayer>, CommandError> {
766 let source = ConfigSource::CommandArg;
767 let mut layers = Vec::new();
768 for (kind, chunk) in &toml_strs.iter().chunk_by(|&(kind, _)| kind) {
769 match kind {
770 ConfigArgKind::Item => {
771 let mut layer = ConfigLayer::empty(source);
772 for (_, item) in chunk {
773 let (name, value) = parse_config_arg_item(item)?;
774 layer.set_value(name, value).map_err(|err| {
777 config_error_with_message("--config argument cannot be set", err)
778 })?;
779 }
780 layers.push(layer);
781 }
782 ConfigArgKind::File => {
783 for (_, path) in chunk {
784 layers.push(ConfigLayer::load_from_file(source, path.into())?);
785 }
786 }
787 }
788 }
789 Ok(layers)
790}
791
792fn parse_config_arg_item(item_str: &str) -> Result<(ConfigNamePathBuf, ConfigValue), CommandError> {
794 let split_candidates = item_str.as_bytes().iter().positions(|&b| b == b'=');
796 let Some((name, value_str)) = split_candidates
797 .map(|p| (&item_str[..p], &item_str[p + 1..]))
798 .map(|(name, value)| name.parse().map(|name| (name, value)))
799 .find_or_last(Result::is_ok)
800 .transpose()
801 .map_err(|err| config_error_with_message("--config name cannot be parsed", err))?
802 else {
803 return Err(config_error("--config must be specified as NAME=VALUE"));
804 };
805 let value = parse_value_or_bare_string(value_str)
806 .map_err(|err| config_error_with_message("--config value cannot be parsed", err))?;
807 Ok((name, value))
808}
809
810pub fn default_config_migrations() -> Vec<ConfigMigrationRule> {
812 vec![
813 ConfigMigrationRule::custom(
815 |layer| {
816 let Ok(Some(val)) = layer.look_up_item("git.auto-local-bookmark") else {
817 return false;
818 };
819 val.as_bool().is_some_and(|b| b)
820 },
821 |_| {
822 Ok("`git.auto-local-bookmark` is deprecated; use \
823 `remotes.<name>.auto-track-bookmarks` instead.
824Example: jj config set --user remotes.origin.auto-track-bookmarks '*'
825For details, see: https://docs.jj-vcs.dev/latest/config/#automatic-tracking-of-bookmarks"
826 .into())
827 },
828 ),
829 ConfigMigrationRule::custom(
831 |layer| {
832 let Ok(Some(val)) = layer.look_up_item("git.push-new-bookmarks") else {
833 return false;
834 };
835 val.as_bool().is_some_and(|b| b)
836 },
837 |_| {
838 Ok("`git.push-new-bookmarks` is deprecated; use \
839 `remotes.<name>.auto-track-bookmarks` instead.
840Example: jj config set --user remotes.origin.auto-track-bookmarks '*'
841For details, see: https://docs.jj-vcs.dev/latest/config/#automatic-tracking-of-bookmarks"
842 .into())
843 },
844 ),
845 ]
846}
847
848#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize)]
850#[serde(untagged)]
851pub enum CommandNameAndArgs {
852 String(String),
853 Vec(NonEmptyCommandArgsVec),
854 Structured {
855 env: HashMap<String, String>,
856 command: NonEmptyCommandArgsVec,
857 },
858}
859
860impl CommandNameAndArgs {
861 pub fn split_name(&self) -> Cow<'_, str> {
863 let (name, _) = self.split_name_and_args();
864 name
865 }
866
867 pub fn split_name_and_args(&self) -> (Cow<'_, str>, Cow<'_, [String]>) {
871 match self {
872 Self::String(s) => {
873 if s.contains('"') || s.contains('\'') {
874 let mut parts = shlex::Shlex::new(s);
875 let res = (
876 parts.next().unwrap_or_default().into(),
877 parts.by_ref().collect(),
878 );
879 if !parts.had_error {
880 return res;
881 }
882 }
883 let mut args = s.split(' ').map(|s| s.to_owned());
884 (args.next().unwrap().into(), args.collect())
885 }
886 Self::Vec(NonEmptyCommandArgsVec(a)) => (Cow::Borrowed(&a[0]), Cow::Borrowed(&a[1..])),
887 Self::Structured {
888 env: _,
889 command: cmd,
890 } => (Cow::Borrowed(&cmd.0[0]), Cow::Borrowed(&cmd.0[1..])),
891 }
892 }
893
894 pub fn as_str(&self) -> Option<&str> {
899 match self {
900 Self::String(s) => Some(s),
901 Self::Vec(_) | Self::Structured { .. } => None,
902 }
903 }
904
905 pub fn to_command(&self) -> Command {
907 let empty: HashMap<&str, &str> = HashMap::new();
908 self.to_command_with_variables(&empty)
909 }
910
911 pub fn to_command_with_variables<V: AsRef<str>>(
914 &self,
915 variables: &HashMap<&str, V>,
916 ) -> Command {
917 let (name, args) = self.split_name_and_args();
918 let mut cmd = Command::new(interpolate_variables_single(name.as_ref(), variables));
919 if let Self::Structured { env, .. } = self {
920 cmd.envs(env);
921 }
922 cmd.args(interpolate_variables(&args, variables));
923 cmd
924 }
925}
926
927impl<T: AsRef<str> + ?Sized> From<&T> for CommandNameAndArgs {
928 fn from(s: &T) -> Self {
929 Self::String(s.as_ref().to_owned())
930 }
931}
932
933impl fmt::Display for CommandNameAndArgs {
934 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
935 match self {
936 Self::String(s) => write!(f, "{s}"),
937 Self::Vec(a) => write!(f, "{}", a.0.join(" ")),
939 Self::Structured { env, command } => {
940 for (k, v) in env {
941 write!(f, "{k}={v} ")?;
942 }
943 write!(f, "{}", command.0.join(" "))
944 }
945 }
946 }
947}
948
949pub fn load_aliases_map<P>(
950 ui: &Ui,
951 config: &StackedConfig,
952 table_name: &ConfigNamePathBuf,
953) -> Result<AliasesMap<P, String>, CommandError>
954where
955 P: AliasDeclarationParser + Default,
956 P::Error: fmt::Display,
957{
958 let mut aliases_map = AliasesMap::new();
959 for layer in config.layers() {
962 let table = match layer.look_up_table(table_name) {
963 Ok(Some(table)) => table,
964 Ok(None) => continue,
965 Err(item) => {
966 return Err(ConfigGetError::Type {
967 name: table_name.to_string(),
968 error: format!("Expected a table, but is {}", item.type_name()).into(),
969 source_path: layer.path.clone(),
970 }
971 .into());
972 }
973 };
974 for (decl, item) in table.iter() {
975 let r = item
976 .as_str()
977 .ok_or_else(|| format!("Expected a string, but is {}", item.type_name()))
978 .and_then(|v| aliases_map.insert(decl, v).map_err(|e| format!("{e}")));
979 if let Err(s) = r {
980 writeln!(
981 ui.warning_default(),
982 "Failed to load `{table_name}.{decl}`: {s}"
983 )?;
984 }
985 }
986 }
987 Ok(aliases_map)
988}
989
990static VARIABLE_REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"\$([a-z0-9_]+)\b").unwrap());
992
993pub fn interpolate_variables<V: AsRef<str>>(
994 args: &[String],
995 variables: &HashMap<&str, V>,
996) -> Vec<String> {
997 args.iter()
998 .map(|arg| interpolate_variables_single(arg, variables))
999 .collect()
1000}
1001
1002fn interpolate_variables_single<V: AsRef<str>>(arg: &str, variables: &HashMap<&str, V>) -> String {
1003 VARIABLE_REGEX
1004 .replace_all(arg, |caps: &Captures| {
1005 let name = &caps[1];
1006 if let Some(subst) = variables.get(name) {
1007 subst.as_ref().to_owned()
1008 } else {
1009 caps[0].to_owned()
1010 }
1011 })
1012 .into_owned()
1013}
1014
1015pub fn find_all_variables(args: &[String]) -> impl Iterator<Item = &str> {
1017 let regex = &*VARIABLE_REGEX;
1018 args.iter()
1019 .flat_map(|arg| regex.find_iter(arg))
1020 .map(|single_match| {
1021 let s = single_match.as_str();
1022 &s[1..]
1023 })
1024}
1025
1026#[derive(Clone, Debug, Eq, Hash, PartialEq, serde::Deserialize)]
1029#[serde(try_from = "Vec<String>")]
1030pub struct NonEmptyCommandArgsVec(Vec<String>);
1031
1032impl TryFrom<Vec<String>> for NonEmptyCommandArgsVec {
1033 type Error = &'static str;
1034
1035 fn try_from(args: Vec<String>) -> Result<Self, Self::Error> {
1036 if args.is_empty() {
1037 Err("command arguments should not be empty")
1038 } else {
1039 Ok(Self(args))
1040 }
1041 }
1042}
1043
1044#[cfg(test)]
1045mod tests {
1046 use std::env::join_paths;
1047 use std::fmt::Write as _;
1048
1049 use indoc::indoc;
1050 use maplit::hashmap;
1051 use test_case::test_case;
1052
1053 use super::*;
1054
1055 fn insta_settings() -> insta::Settings {
1056 let mut settings = insta::Settings::clone_current();
1057 settings.add_filter(r"\bDecor \{[^}]*\}", "Decor { .. }");
1059 settings
1060 }
1061
1062 #[test]
1063 fn test_parse_value_or_bare_string() {
1064 let parse = |s: &str| parse_value_or_bare_string(s);
1065
1066 assert_eq!(parse("true").unwrap().as_bool(), Some(true));
1068 assert_eq!(parse("42").unwrap().as_integer(), Some(42));
1069 assert_eq!(parse("-1").unwrap().as_integer(), Some(-1));
1070 assert_eq!(parse("'a'").unwrap().as_str(), Some("a"));
1071 assert!(parse("[]").unwrap().is_array());
1072 assert!(parse("{ a = 'b' }").unwrap().is_inline_table());
1073
1074 assert_eq!(parse("").unwrap().as_str(), Some(""));
1076 assert_eq!(parse("John Doe").unwrap().as_str(), Some("John Doe"));
1077 assert_eq!(parse("Doe, John").unwrap().as_str(), Some("Doe, John"));
1078 assert_eq!(parse("It's okay").unwrap().as_str(), Some("It's okay"));
1079 assert_eq!(
1080 parse("<foo+bar@example.org>").unwrap().as_str(),
1081 Some("<foo+bar@example.org>")
1082 );
1083 assert_eq!(parse("#ff00aa").unwrap().as_str(), Some("#ff00aa"));
1084 assert_eq!(parse("all()").unwrap().as_str(), Some("all()"));
1085 assert_eq!(parse("glob:*.*").unwrap().as_str(), Some("glob:*.*"));
1086 assert_eq!(parse("柔術").unwrap().as_str(), Some("柔術"));
1087
1088 assert!(parse("'foo").is_err());
1090 assert!(parse(r#" bar" "#).is_err());
1091 assert!(parse("[0 1]").is_err());
1092 assert!(parse("{ x = y }").is_err());
1093 assert!(parse("\n { x").is_err());
1094 assert!(parse(" x ] ").is_err());
1095 assert!(parse("[table]\nkey = 'value'").is_err());
1096 }
1097
1098 #[test]
1099 fn test_parse_config_arg_item() {
1100 assert!(parse_config_arg_item("").is_err());
1101 assert!(parse_config_arg_item("a").is_err());
1102 assert!(parse_config_arg_item("=").is_err());
1103 assert!(parse_config_arg_item("a = 'b'").is_err());
1106
1107 let (name, value) = parse_config_arg_item("a=b").unwrap();
1108 assert_eq!(name, ConfigNamePathBuf::from_iter(["a"]));
1109 assert_eq!(value.as_str(), Some("b"));
1110
1111 let (name, value) = parse_config_arg_item("a=").unwrap();
1112 assert_eq!(name, ConfigNamePathBuf::from_iter(["a"]));
1113 assert_eq!(value.as_str(), Some(""));
1114
1115 let (name, value) = parse_config_arg_item("a= ").unwrap();
1116 assert_eq!(name, ConfigNamePathBuf::from_iter(["a"]));
1117 assert_eq!(value.as_str(), Some(" "));
1118
1119 let (name, value) = parse_config_arg_item("a=b=c").unwrap();
1121 assert_eq!(name, ConfigNamePathBuf::from_iter(["a"]));
1122 assert_eq!(value.as_str(), Some("b=c"));
1123
1124 let (name, value) = parse_config_arg_item("a.b=true").unwrap();
1125 assert_eq!(name, ConfigNamePathBuf::from_iter(["a", "b"]));
1126 assert_eq!(value.as_bool(), Some(true));
1127
1128 let (name, value) = parse_config_arg_item("a='b=c'").unwrap();
1129 assert_eq!(name, ConfigNamePathBuf::from_iter(["a"]));
1130 assert_eq!(value.as_str(), Some("b=c"));
1131
1132 let (name, value) = parse_config_arg_item("'a=b'=c").unwrap();
1133 assert_eq!(name, ConfigNamePathBuf::from_iter(["a=b"]));
1134 assert_eq!(value.as_str(), Some("c"));
1135
1136 let (name, value) = parse_config_arg_item("'a = b=c '={d = 'e=f'}").unwrap();
1137 assert_eq!(name, ConfigNamePathBuf::from_iter(["a = b=c "]));
1138 assert!(value.is_inline_table());
1139 assert_eq!(value.to_string(), "{d = 'e=f'}");
1140 }
1141
1142 #[test]
1143 fn test_command_args() {
1144 let mut config = StackedConfig::empty();
1145 config.add_layer(
1146 ConfigLayer::parse(
1147 ConfigSource::User,
1148 indoc! {"
1149 empty_array = []
1150 empty_string = ''
1151 array = ['emacs', '-nw']
1152 string = 'emacs -nw'
1153 string_quoted = '\"spaced path/to/emacs\" -nw'
1154 structured.env = { KEY1 = 'value1', KEY2 = 'value2' }
1155 structured.command = ['emacs', '-nw']
1156 "},
1157 )
1158 .unwrap(),
1159 );
1160
1161 assert!(config.get::<CommandNameAndArgs>("empty_array").is_err());
1162
1163 let command_args: CommandNameAndArgs = config.get("empty_string").unwrap();
1164 assert_eq!(command_args, CommandNameAndArgs::String("".to_owned()));
1165 let (name, args) = command_args.split_name_and_args();
1166 assert_eq!(name, "");
1167 assert!(args.is_empty());
1168
1169 let command_args: CommandNameAndArgs = config.get("array").unwrap();
1170 assert_eq!(
1171 command_args,
1172 CommandNameAndArgs::Vec(NonEmptyCommandArgsVec(
1173 ["emacs", "-nw",].map(|s| s.to_owned()).to_vec()
1174 ))
1175 );
1176 let (name, args) = command_args.split_name_and_args();
1177 assert_eq!(name, "emacs");
1178 assert_eq!(args, ["-nw"].as_ref());
1179
1180 let command_args: CommandNameAndArgs = config.get("string").unwrap();
1181 assert_eq!(
1182 command_args,
1183 CommandNameAndArgs::String("emacs -nw".to_owned())
1184 );
1185 let (name, args) = command_args.split_name_and_args();
1186 assert_eq!(name, "emacs");
1187 assert_eq!(args, ["-nw"].as_ref());
1188
1189 let command_args: CommandNameAndArgs = config.get("string_quoted").unwrap();
1190 assert_eq!(
1191 command_args,
1192 CommandNameAndArgs::String("\"spaced path/to/emacs\" -nw".to_owned())
1193 );
1194 let (name, args) = command_args.split_name_and_args();
1195 assert_eq!(name, "spaced path/to/emacs");
1196 assert_eq!(args, ["-nw"].as_ref());
1197
1198 let command_args: CommandNameAndArgs = config.get("structured").unwrap();
1199 assert_eq!(
1200 command_args,
1201 CommandNameAndArgs::Structured {
1202 env: hashmap! {
1203 "KEY1".to_string() => "value1".to_string(),
1204 "KEY2".to_string() => "value2".to_string(),
1205 },
1206 command: NonEmptyCommandArgsVec(["emacs", "-nw",].map(|s| s.to_owned()).to_vec())
1207 }
1208 );
1209 let (name, args) = command_args.split_name_and_args();
1210 assert_eq!(name, "emacs");
1211 assert_eq!(args, ["-nw"].as_ref());
1212 }
1213
1214 #[test]
1215 fn test_resolved_config_values_empty() {
1216 let config = StackedConfig::empty();
1217 assert!(resolved_config_values(&config, &ConfigNamePathBuf::root()).is_empty());
1218 }
1219
1220 #[test]
1221 fn test_resolved_config_values_single_key() {
1222 let settings = insta_settings();
1223 let _guard = settings.bind_to_scope();
1224 let mut env_base_layer = ConfigLayer::empty(ConfigSource::EnvBase);
1225 env_base_layer
1226 .set_value("user.name", "base-user-name")
1227 .unwrap();
1228 env_base_layer
1229 .set_value("user.email", "base@user.email")
1230 .unwrap();
1231 let mut repo_layer = ConfigLayer::empty(ConfigSource::Repo);
1232 repo_layer
1233 .set_value("user.email", "repo@user.email")
1234 .unwrap();
1235 let mut config = StackedConfig::empty();
1236 config.add_layer(env_base_layer);
1237 config.add_layer(repo_layer);
1238 insta::assert_debug_snapshot!(
1240 resolved_config_values(&config, &ConfigNamePathBuf::root()),
1241 @r#"
1242 [
1243 AnnotatedValue {
1244 name: ConfigNamePathBuf(
1245 [
1246 Key {
1247 key: "user",
1248 repr: None,
1249 leaf_decor: Decor { .. },
1250 dotted_decor: Decor { .. },
1251 },
1252 Key {
1253 key: "name",
1254 repr: None,
1255 leaf_decor: Decor { .. },
1256 dotted_decor: Decor { .. },
1257 },
1258 ],
1259 ),
1260 value: String(
1261 Formatted {
1262 value: "base-user-name",
1263 repr: "default",
1264 decor: Decor { .. },
1265 },
1266 ),
1267 source: EnvBase,
1268 path: None,
1269 is_overridden: false,
1270 },
1271 AnnotatedValue {
1272 name: ConfigNamePathBuf(
1273 [
1274 Key {
1275 key: "user",
1276 repr: None,
1277 leaf_decor: Decor { .. },
1278 dotted_decor: Decor { .. },
1279 },
1280 Key {
1281 key: "email",
1282 repr: None,
1283 leaf_decor: Decor { .. },
1284 dotted_decor: Decor { .. },
1285 },
1286 ],
1287 ),
1288 value: String(
1289 Formatted {
1290 value: "base@user.email",
1291 repr: "default",
1292 decor: Decor { .. },
1293 },
1294 ),
1295 source: EnvBase,
1296 path: None,
1297 is_overridden: true,
1298 },
1299 AnnotatedValue {
1300 name: ConfigNamePathBuf(
1301 [
1302 Key {
1303 key: "user",
1304 repr: None,
1305 leaf_decor: Decor { .. },
1306 dotted_decor: Decor { .. },
1307 },
1308 Key {
1309 key: "email",
1310 repr: None,
1311 leaf_decor: Decor { .. },
1312 dotted_decor: Decor { .. },
1313 },
1314 ],
1315 ),
1316 value: String(
1317 Formatted {
1318 value: "repo@user.email",
1319 repr: "default",
1320 decor: Decor { .. },
1321 },
1322 ),
1323 source: Repo,
1324 path: None,
1325 is_overridden: false,
1326 },
1327 ]
1328 "#
1329 );
1330 }
1331
1332 #[test]
1333 fn test_resolved_config_values_filter_path() {
1334 let settings = insta_settings();
1335 let _guard = settings.bind_to_scope();
1336 let mut user_layer = ConfigLayer::empty(ConfigSource::User);
1337 user_layer.set_value("test-table1.foo", "user-FOO").unwrap();
1338 user_layer.set_value("test-table2.bar", "user-BAR").unwrap();
1339 let mut repo_layer = ConfigLayer::empty(ConfigSource::Repo);
1340 repo_layer.set_value("test-table1.bar", "repo-BAR").unwrap();
1341 let mut config = StackedConfig::empty();
1342 config.add_layer(user_layer);
1343 config.add_layer(repo_layer);
1344 insta::assert_debug_snapshot!(
1345 resolved_config_values(&config, &ConfigNamePathBuf::from_iter(["test-table1"])),
1346 @r#"
1347 [
1348 AnnotatedValue {
1349 name: ConfigNamePathBuf(
1350 [
1351 Key {
1352 key: "test-table1",
1353 repr: None,
1354 leaf_decor: Decor { .. },
1355 dotted_decor: Decor { .. },
1356 },
1357 Key {
1358 key: "foo",
1359 repr: None,
1360 leaf_decor: Decor { .. },
1361 dotted_decor: Decor { .. },
1362 },
1363 ],
1364 ),
1365 value: String(
1366 Formatted {
1367 value: "user-FOO",
1368 repr: "default",
1369 decor: Decor { .. },
1370 },
1371 ),
1372 source: User,
1373 path: None,
1374 is_overridden: false,
1375 },
1376 AnnotatedValue {
1377 name: ConfigNamePathBuf(
1378 [
1379 Key {
1380 key: "test-table1",
1381 repr: None,
1382 leaf_decor: Decor { .. },
1383 dotted_decor: Decor { .. },
1384 },
1385 Key {
1386 key: "bar",
1387 repr: None,
1388 leaf_decor: Decor { .. },
1389 dotted_decor: Decor { .. },
1390 },
1391 ],
1392 ),
1393 value: String(
1394 Formatted {
1395 value: "repo-BAR",
1396 repr: "default",
1397 decor: Decor { .. },
1398 },
1399 ),
1400 source: Repo,
1401 path: None,
1402 is_overridden: false,
1403 },
1404 ]
1405 "#
1406 );
1407 }
1408
1409 #[test]
1410 fn test_resolved_config_values_overridden() {
1411 let list = |layers: &[&ConfigLayer], prefix: &str| -> String {
1412 let mut config = StackedConfig::empty();
1413 config.extend_layers(layers.iter().copied().cloned());
1414 let prefix = if prefix.is_empty() {
1415 ConfigNamePathBuf::root()
1416 } else {
1417 prefix.parse().unwrap()
1418 };
1419 let mut output = String::new();
1420 for annotated in resolved_config_values(&config, &prefix) {
1421 let AnnotatedValue { name, value, .. } = &annotated;
1422 let sigil = if annotated.is_overridden { '!' } else { ' ' };
1423 writeln!(output, "{sigil}{name} = {value}").unwrap();
1424 }
1425 output
1426 };
1427
1428 let mut layer0 = ConfigLayer::empty(ConfigSource::User);
1429 layer0.set_value("a.b.e", "0.0").unwrap();
1430 layer0.set_value("a.b.c.f", "0.1").unwrap();
1431 layer0.set_value("a.b.d", "0.2").unwrap();
1432 let mut layer1 = ConfigLayer::empty(ConfigSource::User);
1433 layer1.set_value("a.b", "1.0").unwrap();
1434 layer1.set_value("a.c", "1.1").unwrap();
1435 let mut layer2 = ConfigLayer::empty(ConfigSource::User);
1436 layer2.set_value("a.b.g", "2.0").unwrap();
1437 layer2.set_value("a.b.d", "2.1").unwrap();
1438
1439 let layers = [&layer0, &layer1];
1441 insta::assert_snapshot!(list(&layers, ""), @r#"
1442 !a.b.e = "0.0"
1443 !a.b.c.f = "0.1"
1444 !a.b.d = "0.2"
1445 a.b = "1.0"
1446 a.c = "1.1"
1447 "#);
1448 insta::assert_snapshot!(list(&layers, "a.b"), @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 "#);
1454 insta::assert_snapshot!(list(&layers, "a.b.c"), @r#"!a.b.c.f = "0.1""#);
1455 insta::assert_snapshot!(list(&layers, "a.b.d"), @r#"!a.b.d = "0.2""#);
1456
1457 let layers = [&layer1, &layer2];
1459 insta::assert_snapshot!(list(&layers, ""), @r#"
1460 !a.b = "1.0"
1461 a.c = "1.1"
1462 a.b.g = "2.0"
1463 a.b.d = "2.1"
1464 "#);
1465 insta::assert_snapshot!(list(&layers, "a.b"), @r#"
1466 !a.b = "1.0"
1467 a.b.g = "2.0"
1468 a.b.d = "2.1"
1469 "#);
1470
1471 let layers = [&layer0, &layer2];
1473 insta::assert_snapshot!(list(&layers, ""), @r#"
1474 a.b.e = "0.0"
1475 a.b.c.f = "0.1"
1476 !a.b.d = "0.2"
1477 a.b.g = "2.0"
1478 a.b.d = "2.1"
1479 "#);
1480 insta::assert_snapshot!(list(&layers, "a.b"), @r#"
1481 a.b.e = "0.0"
1482 a.b.c.f = "0.1"
1483 !a.b.d = "0.2"
1484 a.b.g = "2.0"
1485 a.b.d = "2.1"
1486 "#);
1487 insta::assert_snapshot!(list(&layers, "a.b.c"), @r#" a.b.c.f = "0.1""#);
1488 insta::assert_snapshot!(list(&layers, "a.b.d"), @r#"
1489 !a.b.d = "0.2"
1490 a.b.d = "2.1"
1491 "#);
1492
1493 let layers = [&layer0, &layer1, &layer2];
1495 insta::assert_snapshot!(list(&layers, ""), @r#"
1496 !a.b.e = "0.0"
1497 !a.b.c.f = "0.1"
1498 !a.b.d = "0.2"
1499 !a.b = "1.0"
1500 a.c = "1.1"
1501 a.b.g = "2.0"
1502 a.b.d = "2.1"
1503 "#);
1504 insta::assert_snapshot!(list(&layers, "a.b"), @r#"
1505 !a.b.e = "0.0"
1506 !a.b.c.f = "0.1"
1507 !a.b.d = "0.2"
1508 !a.b = "1.0"
1509 a.b.g = "2.0"
1510 a.b.d = "2.1"
1511 "#);
1512 insta::assert_snapshot!(list(&layers, "a.b.c"), @r#"!a.b.c.f = "0.1""#);
1513 }
1514
1515 struct TestCase {
1516 files: &'static [&'static str],
1517 env: UnresolvedConfigEnv,
1518 wants: Vec<Want>,
1519 }
1520
1521 #[derive(Debug)]
1522 enum WantState {
1523 New,
1524 Existing,
1525 }
1526 #[derive(Debug)]
1527 struct Want {
1528 path: &'static str,
1529 state: WantState,
1530 }
1531
1532 impl Want {
1533 const fn new(path: &'static str) -> Self {
1534 Self {
1535 path,
1536 state: WantState::New,
1537 }
1538 }
1539
1540 const fn existing(path: &'static str) -> Self {
1541 Self {
1542 path,
1543 state: WantState::Existing,
1544 }
1545 }
1546
1547 fn rooted_path(&self, root: &Path) -> PathBuf {
1548 root.join(self.path)
1549 }
1550
1551 fn exists(&self) -> bool {
1552 matches!(self.state, WantState::Existing)
1553 }
1554 }
1555
1556 fn config_path_home_existing() -> TestCase {
1557 TestCase {
1558 files: &["home/.jjconfig.toml"],
1559 env: UnresolvedConfigEnv {
1560 home_dir: Some("home".into()),
1561 ..Default::default()
1562 },
1563 wants: vec![Want::existing("home/.jjconfig.toml")],
1564 }
1565 }
1566
1567 fn config_path_home_new() -> TestCase {
1568 TestCase {
1569 files: &[],
1570 env: UnresolvedConfigEnv {
1571 home_dir: Some("home".into()),
1572 ..Default::default()
1573 },
1574 wants: vec![Want::new("home/.jjconfig.toml")],
1575 }
1576 }
1577
1578 fn config_path_home_existing_platform_new() -> TestCase {
1579 TestCase {
1580 files: &["home/.jjconfig.toml"],
1581 env: UnresolvedConfigEnv {
1582 home_dir: Some("home".into()),
1583 config_dir: Some("config".into()),
1584 ..Default::default()
1585 },
1586 wants: vec![
1587 Want::existing("home/.jjconfig.toml"),
1588 Want::new("config/jj/config.toml"),
1589 ],
1590 }
1591 }
1592
1593 fn config_path_platform_existing() -> TestCase {
1594 TestCase {
1595 files: &["config/jj/config.toml"],
1596 env: UnresolvedConfigEnv {
1597 home_dir: Some("home".into()),
1598 config_dir: Some("config".into()),
1599 ..Default::default()
1600 },
1601 wants: vec![Want::existing("config/jj/config.toml")],
1602 }
1603 }
1604
1605 fn config_path_platform_new() -> TestCase {
1606 TestCase {
1607 files: &[],
1608 env: UnresolvedConfigEnv {
1609 config_dir: Some("config".into()),
1610 ..Default::default()
1611 },
1612 wants: vec![Want::new("config/jj/config.toml")],
1613 }
1614 }
1615
1616 fn config_path_new_prefer_platform() -> TestCase {
1617 TestCase {
1618 files: &[],
1619 env: UnresolvedConfigEnv {
1620 home_dir: Some("home".into()),
1621 config_dir: Some("config".into()),
1622 ..Default::default()
1623 },
1624 wants: vec![Want::new("config/jj/config.toml")],
1625 }
1626 }
1627
1628 fn config_path_jj_config_existing() -> TestCase {
1629 TestCase {
1630 files: &["custom.toml"],
1631 env: UnresolvedConfigEnv {
1632 jj_config: Some("custom.toml".into()),
1633 ..Default::default()
1634 },
1635 wants: vec![Want::existing("custom.toml")],
1636 }
1637 }
1638
1639 fn config_path_jj_config_new() -> TestCase {
1640 TestCase {
1641 files: &[],
1642 env: UnresolvedConfigEnv {
1643 jj_config: Some("custom.toml".into()),
1644 ..Default::default()
1645 },
1646 wants: vec![Want::new("custom.toml")],
1647 }
1648 }
1649
1650 fn config_path_jj_config_existing_multiple() -> TestCase {
1651 TestCase {
1652 files: &["custom1.toml", "custom2.toml"],
1653 env: UnresolvedConfigEnv {
1654 jj_config: Some(
1655 join_paths(["custom1.toml", "custom2.toml"])
1656 .unwrap()
1657 .into_string()
1658 .unwrap(),
1659 ),
1660 ..Default::default()
1661 },
1662 wants: vec![
1663 Want::existing("custom1.toml"),
1664 Want::existing("custom2.toml"),
1665 ],
1666 }
1667 }
1668
1669 fn config_path_jj_config_new_multiple() -> TestCase {
1670 TestCase {
1671 files: &["custom1.toml"],
1672 env: UnresolvedConfigEnv {
1673 jj_config: Some(
1674 join_paths(["custom1.toml", "custom2.toml"])
1675 .unwrap()
1676 .into_string()
1677 .unwrap(),
1678 ),
1679 ..Default::default()
1680 },
1681 wants: vec![Want::existing("custom1.toml"), Want::new("custom2.toml")],
1682 }
1683 }
1684
1685 fn config_path_jj_config_empty_paths_filtered() -> TestCase {
1686 TestCase {
1687 files: &["custom1.toml"],
1688 env: UnresolvedConfigEnv {
1689 jj_config: Some(
1690 join_paths(["custom1.toml", "", "custom2.toml"])
1691 .unwrap()
1692 .into_string()
1693 .unwrap(),
1694 ),
1695 ..Default::default()
1696 },
1697 wants: vec![Want::existing("custom1.toml"), Want::new("custom2.toml")],
1698 }
1699 }
1700
1701 fn config_path_jj_config_empty() -> TestCase {
1702 TestCase {
1703 files: &[],
1704 env: UnresolvedConfigEnv {
1705 jj_config: Some("".to_owned()),
1706 ..Default::default()
1707 },
1708 wants: vec![],
1709 }
1710 }
1711
1712 fn config_path_config_pick_platform() -> TestCase {
1713 TestCase {
1714 files: &["config/jj/config.toml"],
1715 env: UnresolvedConfigEnv {
1716 home_dir: Some("home".into()),
1717 config_dir: Some("config".into()),
1718 ..Default::default()
1719 },
1720 wants: vec![Want::existing("config/jj/config.toml")],
1721 }
1722 }
1723
1724 fn config_path_config_pick_home() -> TestCase {
1725 TestCase {
1726 files: &["home/.jjconfig.toml"],
1727 env: UnresolvedConfigEnv {
1728 home_dir: Some("home".into()),
1729 config_dir: Some("config".into()),
1730 ..Default::default()
1731 },
1732 wants: vec![
1733 Want::existing("home/.jjconfig.toml"),
1734 Want::new("config/jj/config.toml"),
1735 ],
1736 }
1737 }
1738
1739 fn config_path_platform_new_conf_dir_existing() -> TestCase {
1740 TestCase {
1741 files: &["config/jj/conf.d/_"],
1742 env: UnresolvedConfigEnv {
1743 home_dir: Some("home".into()),
1744 config_dir: Some("config".into()),
1745 ..Default::default()
1746 },
1747 wants: vec![
1748 Want::new("config/jj/config.toml"),
1749 Want::existing("config/jj/conf.d"),
1750 ],
1751 }
1752 }
1753
1754 fn config_path_platform_existing_conf_dir_existing() -> TestCase {
1755 TestCase {
1756 files: &["config/jj/config.toml", "config/jj/conf.d/_"],
1757 env: UnresolvedConfigEnv {
1758 home_dir: Some("home".into()),
1759 config_dir: Some("config".into()),
1760 ..Default::default()
1761 },
1762 wants: vec![
1763 Want::existing("config/jj/config.toml"),
1764 Want::existing("config/jj/conf.d"),
1765 ],
1766 }
1767 }
1768
1769 fn config_path_all_existing() -> TestCase {
1770 TestCase {
1771 files: &[
1772 "config/jj/conf.d/_",
1773 "config/jj/config.toml",
1774 "home/.jjconfig.toml",
1775 ],
1776 env: UnresolvedConfigEnv {
1777 home_dir: Some("home".into()),
1778 config_dir: Some("config".into()),
1779 ..Default::default()
1780 },
1781 wants: vec![
1783 Want::existing("home/.jjconfig.toml"),
1784 Want::existing("config/jj/config.toml"),
1785 Want::existing("config/jj/conf.d"),
1786 ],
1787 }
1788 }
1789
1790 fn config_path_none() -> TestCase {
1791 TestCase {
1792 files: &[],
1793 env: Default::default(),
1794 wants: vec![],
1795 }
1796 }
1797
1798 #[test_case(config_path_home_existing())]
1799 #[test_case(config_path_home_new())]
1800 #[test_case(config_path_home_existing_platform_new())]
1801 #[test_case(config_path_platform_existing())]
1802 #[test_case(config_path_platform_new())]
1803 #[test_case(config_path_new_prefer_platform())]
1804 #[test_case(config_path_jj_config_existing())]
1805 #[test_case(config_path_jj_config_new())]
1806 #[test_case(config_path_jj_config_existing_multiple())]
1807 #[test_case(config_path_jj_config_new_multiple())]
1808 #[test_case(config_path_jj_config_empty_paths_filtered())]
1809 #[test_case(config_path_jj_config_empty())]
1810 #[test_case(config_path_config_pick_platform())]
1811 #[test_case(config_path_config_pick_home())]
1812 #[test_case(config_path_platform_new_conf_dir_existing())]
1813 #[test_case(config_path_platform_existing_conf_dir_existing())]
1814 #[test_case(config_path_all_existing())]
1815 #[test_case(config_path_none())]
1816 fn test_config_path(case: TestCase) {
1817 let tmp = setup_config_fs(case.files);
1818 let env = resolve_config_env(&case.env, tmp.path());
1819
1820 let all_expected_paths = case
1821 .wants
1822 .iter()
1823 .map(|w| w.rooted_path(tmp.path()))
1824 .collect_vec();
1825 let exists_expected_paths = case
1826 .wants
1827 .iter()
1828 .filter(|w| w.exists())
1829 .map(|w| w.rooted_path(tmp.path()))
1830 .collect_vec();
1831
1832 let all_paths = env.user_config_paths().collect_vec();
1833 let exists_paths = env.existing_user_config_paths().collect_vec();
1834
1835 assert_eq!(all_paths, all_expected_paths);
1836 assert_eq!(exists_paths, exists_expected_paths);
1837 }
1838
1839 fn setup_config_fs(files: &[&str]) -> tempfile::TempDir {
1840 let tmp = testutils::new_temp_dir();
1841 for file in files {
1842 let path = tmp.path().join(file);
1843 if let Some(parent) = path.parent() {
1844 std::fs::create_dir_all(parent).unwrap();
1845 }
1846 std::fs::File::create(path).unwrap();
1847 }
1848 tmp
1849 }
1850
1851 fn resolve_config_env(env: &UnresolvedConfigEnv, root: &Path) -> ConfigEnv {
1852 let home_dir = env.home_dir.as_ref().map(|p| root.join(p));
1853 let env = UnresolvedConfigEnv {
1854 config_dir: env.config_dir.as_ref().map(|p| root.join(p)),
1855 home_dir: home_dir.clone(),
1856 jj_config: env.jj_config.as_ref().map(|p| {
1857 join_paths(split_paths(p).map(|p| {
1858 if p.as_os_str().is_empty() {
1859 return p;
1860 }
1861 root.join(p)
1862 }))
1863 .unwrap()
1864 .into_string()
1865 .unwrap()
1866 }),
1867 };
1868 ConfigEnv {
1869 home_dir,
1870 root_config_dir: None,
1871 repo_path: None,
1872 workspace_path: None,
1873 user_config_paths: env.resolve(),
1874 repo_config: None,
1875 workspace_config: None,
1876 command: None,
1877 hostname: None,
1878 environment: HashMap::new(),
1879 rng: Arc::new(Mutex::new(ChaCha20Rng::seed_from_u64(0))),
1880 }
1881 }
1882}