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 use testutils::TestResult;
1053
1054 use super::*;
1055
1056 fn insta_settings() -> insta::Settings {
1057 let mut settings = insta::Settings::clone_current();
1058 settings.add_filter(r"\bDecor \{[^}]*\}", "Decor { .. }");
1060 settings
1061 }
1062
1063 #[test]
1064 fn test_parse_value_or_bare_string() -> TestResult {
1065 let parse = |s: &str| parse_value_or_bare_string(s);
1066
1067 assert_eq!(parse("true")?.as_bool(), Some(true));
1069 assert_eq!(parse("42")?.as_integer(), Some(42));
1070 assert_eq!(parse("-1")?.as_integer(), Some(-1));
1071 assert_eq!(parse("'a'")?.as_str(), Some("a"));
1072 assert!(parse("[]")?.is_array());
1073 assert!(parse("{ a = 'b' }")?.is_inline_table());
1074
1075 assert_eq!(parse("")?.as_str(), Some(""));
1077 assert_eq!(parse("John Doe")?.as_str(), Some("John Doe"));
1078 assert_eq!(parse("Doe, John")?.as_str(), Some("Doe, John"));
1079 assert_eq!(parse("It's okay")?.as_str(), Some("It's okay"));
1080 assert_eq!(
1081 parse("<foo+bar@example.org>")?.as_str(),
1082 Some("<foo+bar@example.org>")
1083 );
1084 assert_eq!(parse("#ff00aa")?.as_str(), Some("#ff00aa"));
1085 assert_eq!(parse("all()")?.as_str(), Some("all()"));
1086 assert_eq!(parse("glob:*.*")?.as_str(), Some("glob:*.*"));
1087 assert_eq!(parse("柔術")?.as_str(), Some("柔術"));
1088
1089 assert!(parse("'foo").is_err());
1091 assert!(parse(r#" bar" "#).is_err());
1092 assert!(parse("[0 1]").is_err());
1093 assert!(parse("{ x = y }").is_err());
1094 assert!(parse("\n { x").is_err());
1095 assert!(parse(" x ] ").is_err());
1096 assert!(parse("[table]\nkey = 'value'").is_err());
1097 Ok(())
1098 }
1099
1100 #[test]
1101 fn test_parse_config_arg_item() {
1102 assert!(parse_config_arg_item("").is_err());
1103 assert!(parse_config_arg_item("a").is_err());
1104 assert!(parse_config_arg_item("=").is_err());
1105 assert!(parse_config_arg_item("a = 'b'").is_err());
1108
1109 let (name, value) = parse_config_arg_item("a=b").unwrap();
1110 assert_eq!(name, ConfigNamePathBuf::from_iter(["a"]));
1111 assert_eq!(value.as_str(), Some("b"));
1112
1113 let (name, value) = parse_config_arg_item("a=").unwrap();
1114 assert_eq!(name, ConfigNamePathBuf::from_iter(["a"]));
1115 assert_eq!(value.as_str(), Some(""));
1116
1117 let (name, value) = parse_config_arg_item("a= ").unwrap();
1118 assert_eq!(name, ConfigNamePathBuf::from_iter(["a"]));
1119 assert_eq!(value.as_str(), Some(" "));
1120
1121 let (name, value) = parse_config_arg_item("a=b=c").unwrap();
1123 assert_eq!(name, ConfigNamePathBuf::from_iter(["a"]));
1124 assert_eq!(value.as_str(), Some("b=c"));
1125
1126 let (name, value) = parse_config_arg_item("a.b=true").unwrap();
1127 assert_eq!(name, ConfigNamePathBuf::from_iter(["a", "b"]));
1128 assert_eq!(value.as_bool(), Some(true));
1129
1130 let (name, value) = parse_config_arg_item("a='b=c'").unwrap();
1131 assert_eq!(name, ConfigNamePathBuf::from_iter(["a"]));
1132 assert_eq!(value.as_str(), Some("b=c"));
1133
1134 let (name, value) = parse_config_arg_item("'a=b'=c").unwrap();
1135 assert_eq!(name, ConfigNamePathBuf::from_iter(["a=b"]));
1136 assert_eq!(value.as_str(), Some("c"));
1137
1138 let (name, value) = parse_config_arg_item("'a = b=c '={d = 'e=f'}").unwrap();
1139 assert_eq!(name, ConfigNamePathBuf::from_iter(["a = b=c "]));
1140 assert!(value.is_inline_table());
1141 assert_eq!(value.to_string(), "{d = 'e=f'}");
1142 }
1143
1144 #[test]
1145 fn test_command_args() -> TestResult {
1146 let mut config = StackedConfig::empty();
1147 config.add_layer(ConfigLayer::parse(
1148 ConfigSource::User,
1149 indoc! {"
1150 empty_array = []
1151 empty_string = ''
1152 array = ['emacs', '-nw']
1153 string = 'emacs -nw'
1154 string_quoted = '\"spaced path/to/emacs\" -nw'
1155 structured.env = { KEY1 = 'value1', KEY2 = 'value2' }
1156 structured.command = ['emacs', '-nw']
1157 "},
1158 )?);
1159
1160 assert!(config.get::<CommandNameAndArgs>("empty_array").is_err());
1161
1162 let command_args: CommandNameAndArgs = config.get("empty_string")?;
1163 assert_eq!(command_args, CommandNameAndArgs::String("".to_owned()));
1164 let (name, args) = command_args.split_name_and_args();
1165 assert_eq!(name, "");
1166 assert!(args.is_empty());
1167
1168 let command_args: CommandNameAndArgs = config.get("array")?;
1169 assert_eq!(
1170 command_args,
1171 CommandNameAndArgs::Vec(NonEmptyCommandArgsVec(
1172 ["emacs", "-nw",].map(|s| s.to_owned()).to_vec()
1173 ))
1174 );
1175 let (name, args) = command_args.split_name_and_args();
1176 assert_eq!(name, "emacs");
1177 assert_eq!(args, ["-nw"].as_ref());
1178
1179 let command_args: CommandNameAndArgs = config.get("string")?;
1180 assert_eq!(
1181 command_args,
1182 CommandNameAndArgs::String("emacs -nw".to_owned())
1183 );
1184 let (name, args) = command_args.split_name_and_args();
1185 assert_eq!(name, "emacs");
1186 assert_eq!(args, ["-nw"].as_ref());
1187
1188 let command_args: CommandNameAndArgs = config.get("string_quoted")?;
1189 assert_eq!(
1190 command_args,
1191 CommandNameAndArgs::String("\"spaced path/to/emacs\" -nw".to_owned())
1192 );
1193 let (name, args) = command_args.split_name_and_args();
1194 assert_eq!(name, "spaced path/to/emacs");
1195 assert_eq!(args, ["-nw"].as_ref());
1196
1197 let command_args: CommandNameAndArgs = config.get("structured")?;
1198 assert_eq!(
1199 command_args,
1200 CommandNameAndArgs::Structured {
1201 env: hashmap! {
1202 "KEY1".to_string() => "value1".to_string(),
1203 "KEY2".to_string() => "value2".to_string(),
1204 },
1205 command: NonEmptyCommandArgsVec(["emacs", "-nw",].map(|s| s.to_owned()).to_vec())
1206 }
1207 );
1208 let (name, args) = command_args.split_name_and_args();
1209 assert_eq!(name, "emacs");
1210 assert_eq!(args, ["-nw"].as_ref());
1211 Ok(())
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() -> TestResult {
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.set_value("user.name", "base-user-name")?;
1226 env_base_layer.set_value("user.email", "base@user.email")?;
1227 let mut repo_layer = ConfigLayer::empty(ConfigSource::Repo);
1228 repo_layer.set_value("user.email", "repo@user.email")?;
1229 let mut config = StackedConfig::empty();
1230 config.add_layer(env_base_layer);
1231 config.add_layer(repo_layer);
1232 insta::assert_debug_snapshot!(
1234 resolved_config_values(&config, &ConfigNamePathBuf::root()),
1235 @r#"
1236 [
1237 AnnotatedValue {
1238 name: ConfigNamePathBuf(
1239 [
1240 Key {
1241 key: "user",
1242 repr: None,
1243 leaf_decor: Decor { .. },
1244 dotted_decor: Decor { .. },
1245 },
1246 Key {
1247 key: "name",
1248 repr: None,
1249 leaf_decor: Decor { .. },
1250 dotted_decor: Decor { .. },
1251 },
1252 ],
1253 ),
1254 value: String(
1255 Formatted {
1256 value: "base-user-name",
1257 repr: "default",
1258 decor: Decor { .. },
1259 },
1260 ),
1261 source: EnvBase,
1262 path: None,
1263 is_overridden: false,
1264 },
1265 AnnotatedValue {
1266 name: ConfigNamePathBuf(
1267 [
1268 Key {
1269 key: "user",
1270 repr: None,
1271 leaf_decor: Decor { .. },
1272 dotted_decor: Decor { .. },
1273 },
1274 Key {
1275 key: "email",
1276 repr: None,
1277 leaf_decor: Decor { .. },
1278 dotted_decor: Decor { .. },
1279 },
1280 ],
1281 ),
1282 value: String(
1283 Formatted {
1284 value: "base@user.email",
1285 repr: "default",
1286 decor: Decor { .. },
1287 },
1288 ),
1289 source: EnvBase,
1290 path: None,
1291 is_overridden: true,
1292 },
1293 AnnotatedValue {
1294 name: ConfigNamePathBuf(
1295 [
1296 Key {
1297 key: "user",
1298 repr: None,
1299 leaf_decor: Decor { .. },
1300 dotted_decor: Decor { .. },
1301 },
1302 Key {
1303 key: "email",
1304 repr: None,
1305 leaf_decor: Decor { .. },
1306 dotted_decor: Decor { .. },
1307 },
1308 ],
1309 ),
1310 value: String(
1311 Formatted {
1312 value: "repo@user.email",
1313 repr: "default",
1314 decor: Decor { .. },
1315 },
1316 ),
1317 source: Repo,
1318 path: None,
1319 is_overridden: false,
1320 },
1321 ]
1322 "#
1323 );
1324 Ok(())
1325 }
1326
1327 #[test]
1328 fn test_resolved_config_values_filter_path() -> TestResult {
1329 let settings = insta_settings();
1330 let _guard = settings.bind_to_scope();
1331 let mut user_layer = ConfigLayer::empty(ConfigSource::User);
1332 user_layer.set_value("test-table1.foo", "user-FOO")?;
1333 user_layer.set_value("test-table2.bar", "user-BAR")?;
1334 let mut repo_layer = ConfigLayer::empty(ConfigSource::Repo);
1335 repo_layer.set_value("test-table1.bar", "repo-BAR")?;
1336 let mut config = StackedConfig::empty();
1337 config.add_layer(user_layer);
1338 config.add_layer(repo_layer);
1339 insta::assert_debug_snapshot!(
1340 resolved_config_values(&config, &ConfigNamePathBuf::from_iter(["test-table1"])),
1341 @r#"
1342 [
1343 AnnotatedValue {
1344 name: ConfigNamePathBuf(
1345 [
1346 Key {
1347 key: "test-table1",
1348 repr: None,
1349 leaf_decor: Decor { .. },
1350 dotted_decor: Decor { .. },
1351 },
1352 Key {
1353 key: "foo",
1354 repr: None,
1355 leaf_decor: Decor { .. },
1356 dotted_decor: Decor { .. },
1357 },
1358 ],
1359 ),
1360 value: String(
1361 Formatted {
1362 value: "user-FOO",
1363 repr: "default",
1364 decor: Decor { .. },
1365 },
1366 ),
1367 source: User,
1368 path: None,
1369 is_overridden: false,
1370 },
1371 AnnotatedValue {
1372 name: ConfigNamePathBuf(
1373 [
1374 Key {
1375 key: "test-table1",
1376 repr: None,
1377 leaf_decor: Decor { .. },
1378 dotted_decor: Decor { .. },
1379 },
1380 Key {
1381 key: "bar",
1382 repr: None,
1383 leaf_decor: Decor { .. },
1384 dotted_decor: Decor { .. },
1385 },
1386 ],
1387 ),
1388 value: String(
1389 Formatted {
1390 value: "repo-BAR",
1391 repr: "default",
1392 decor: Decor { .. },
1393 },
1394 ),
1395 source: Repo,
1396 path: None,
1397 is_overridden: false,
1398 },
1399 ]
1400 "#
1401 );
1402 Ok(())
1403 }
1404
1405 #[test]
1406 fn test_resolved_config_values_overridden() -> TestResult {
1407 let list = |layers: &[&ConfigLayer], prefix: &str| -> String {
1408 let mut config = StackedConfig::empty();
1409 config.extend_layers(layers.iter().copied().cloned());
1410 let prefix = if prefix.is_empty() {
1411 ConfigNamePathBuf::root()
1412 } else {
1413 prefix.parse().unwrap()
1414 };
1415 let mut output = String::new();
1416 for annotated in resolved_config_values(&config, &prefix) {
1417 let AnnotatedValue { name, value, .. } = &annotated;
1418 let sigil = if annotated.is_overridden { '!' } else { ' ' };
1419 writeln!(output, "{sigil}{name} = {value}").unwrap();
1420 }
1421 output
1422 };
1423
1424 let mut layer0 = ConfigLayer::empty(ConfigSource::User);
1425 layer0.set_value("a.b.e", "0.0")?;
1426 layer0.set_value("a.b.c.f", "0.1")?;
1427 layer0.set_value("a.b.d", "0.2")?;
1428 let mut layer1 = ConfigLayer::empty(ConfigSource::User);
1429 layer1.set_value("a.b", "1.0")?;
1430 layer1.set_value("a.c", "1.1")?;
1431 let mut layer2 = ConfigLayer::empty(ConfigSource::User);
1432 layer2.set_value("a.b.g", "2.0")?;
1433 layer2.set_value("a.b.d", "2.1")?;
1434
1435 let layers = [&layer0, &layer1];
1437 insta::assert_snapshot!(list(&layers, ""), @r#"
1438 !a.b.e = "0.0"
1439 !a.b.c.f = "0.1"
1440 !a.b.d = "0.2"
1441 a.b = "1.0"
1442 a.c = "1.1"
1443 "#);
1444 insta::assert_snapshot!(list(&layers, "a.b"), @r#"
1445 !a.b.e = "0.0"
1446 !a.b.c.f = "0.1"
1447 !a.b.d = "0.2"
1448 a.b = "1.0"
1449 "#);
1450 insta::assert_snapshot!(list(&layers, "a.b.c"), @r#"!a.b.c.f = "0.1""#);
1451 insta::assert_snapshot!(list(&layers, "a.b.d"), @r#"!a.b.d = "0.2""#);
1452
1453 let layers = [&layer1, &layer2];
1455 insta::assert_snapshot!(list(&layers, ""), @r#"
1456 !a.b = "1.0"
1457 a.c = "1.1"
1458 a.b.g = "2.0"
1459 a.b.d = "2.1"
1460 "#);
1461 insta::assert_snapshot!(list(&layers, "a.b"), @r#"
1462 !a.b = "1.0"
1463 a.b.g = "2.0"
1464 a.b.d = "2.1"
1465 "#);
1466
1467 let layers = [&layer0, &layer2];
1469 insta::assert_snapshot!(list(&layers, ""), @r#"
1470 a.b.e = "0.0"
1471 a.b.c.f = "0.1"
1472 !a.b.d = "0.2"
1473 a.b.g = "2.0"
1474 a.b.d = "2.1"
1475 "#);
1476 insta::assert_snapshot!(list(&layers, "a.b"), @r#"
1477 a.b.e = "0.0"
1478 a.b.c.f = "0.1"
1479 !a.b.d = "0.2"
1480 a.b.g = "2.0"
1481 a.b.d = "2.1"
1482 "#);
1483 insta::assert_snapshot!(list(&layers, "a.b.c"), @r#" a.b.c.f = "0.1""#);
1484 insta::assert_snapshot!(list(&layers, "a.b.d"), @r#"
1485 !a.b.d = "0.2"
1486 a.b.d = "2.1"
1487 "#);
1488
1489 let layers = [&layer0, &layer1, &layer2];
1491 insta::assert_snapshot!(list(&layers, ""), @r#"
1492 !a.b.e = "0.0"
1493 !a.b.c.f = "0.1"
1494 !a.b.d = "0.2"
1495 !a.b = "1.0"
1496 a.c = "1.1"
1497 a.b.g = "2.0"
1498 a.b.d = "2.1"
1499 "#);
1500 insta::assert_snapshot!(list(&layers, "a.b"), @r#"
1501 !a.b.e = "0.0"
1502 !a.b.c.f = "0.1"
1503 !a.b.d = "0.2"
1504 !a.b = "1.0"
1505 a.b.g = "2.0"
1506 a.b.d = "2.1"
1507 "#);
1508 insta::assert_snapshot!(list(&layers, "a.b.c"), @r#"!a.b.c.f = "0.1""#);
1509 Ok(())
1510 }
1511
1512 struct TestCase {
1513 files: &'static [&'static str],
1514 env: UnresolvedConfigEnv,
1515 wants: Vec<Want>,
1516 }
1517
1518 #[derive(Debug)]
1519 enum WantState {
1520 New,
1521 Existing,
1522 }
1523 #[derive(Debug)]
1524 struct Want {
1525 path: &'static str,
1526 state: WantState,
1527 }
1528
1529 impl Want {
1530 const fn new(path: &'static str) -> Self {
1531 Self {
1532 path,
1533 state: WantState::New,
1534 }
1535 }
1536
1537 const fn existing(path: &'static str) -> Self {
1538 Self {
1539 path,
1540 state: WantState::Existing,
1541 }
1542 }
1543
1544 fn rooted_path(&self, root: &Path) -> PathBuf {
1545 root.join(self.path)
1546 }
1547
1548 fn exists(&self) -> bool {
1549 matches!(self.state, WantState::Existing)
1550 }
1551 }
1552
1553 fn config_path_home_existing() -> TestCase {
1554 TestCase {
1555 files: &["home/.jjconfig.toml"],
1556 env: UnresolvedConfigEnv {
1557 home_dir: Some("home".into()),
1558 ..Default::default()
1559 },
1560 wants: vec![Want::existing("home/.jjconfig.toml")],
1561 }
1562 }
1563
1564 fn config_path_home_new() -> TestCase {
1565 TestCase {
1566 files: &[],
1567 env: UnresolvedConfigEnv {
1568 home_dir: Some("home".into()),
1569 ..Default::default()
1570 },
1571 wants: vec![Want::new("home/.jjconfig.toml")],
1572 }
1573 }
1574
1575 fn config_path_home_existing_platform_new() -> TestCase {
1576 TestCase {
1577 files: &["home/.jjconfig.toml"],
1578 env: UnresolvedConfigEnv {
1579 home_dir: Some("home".into()),
1580 config_dir: Some("config".into()),
1581 ..Default::default()
1582 },
1583 wants: vec![
1584 Want::existing("home/.jjconfig.toml"),
1585 Want::new("config/jj/config.toml"),
1586 ],
1587 }
1588 }
1589
1590 fn config_path_platform_existing() -> TestCase {
1591 TestCase {
1592 files: &["config/jj/config.toml"],
1593 env: UnresolvedConfigEnv {
1594 home_dir: Some("home".into()),
1595 config_dir: Some("config".into()),
1596 ..Default::default()
1597 },
1598 wants: vec![Want::existing("config/jj/config.toml")],
1599 }
1600 }
1601
1602 fn config_path_platform_new() -> TestCase {
1603 TestCase {
1604 files: &[],
1605 env: UnresolvedConfigEnv {
1606 config_dir: Some("config".into()),
1607 ..Default::default()
1608 },
1609 wants: vec![Want::new("config/jj/config.toml")],
1610 }
1611 }
1612
1613 fn config_path_new_prefer_platform() -> TestCase {
1614 TestCase {
1615 files: &[],
1616 env: UnresolvedConfigEnv {
1617 home_dir: Some("home".into()),
1618 config_dir: Some("config".into()),
1619 ..Default::default()
1620 },
1621 wants: vec![Want::new("config/jj/config.toml")],
1622 }
1623 }
1624
1625 fn config_path_jj_config_existing() -> TestCase {
1626 TestCase {
1627 files: &["custom.toml"],
1628 env: UnresolvedConfigEnv {
1629 jj_config: Some("custom.toml".into()),
1630 ..Default::default()
1631 },
1632 wants: vec![Want::existing("custom.toml")],
1633 }
1634 }
1635
1636 fn config_path_jj_config_new() -> TestCase {
1637 TestCase {
1638 files: &[],
1639 env: UnresolvedConfigEnv {
1640 jj_config: Some("custom.toml".into()),
1641 ..Default::default()
1642 },
1643 wants: vec![Want::new("custom.toml")],
1644 }
1645 }
1646
1647 fn config_path_jj_config_existing_multiple() -> TestCase {
1648 TestCase {
1649 files: &["custom1.toml", "custom2.toml"],
1650 env: UnresolvedConfigEnv {
1651 jj_config: Some(
1652 join_paths(["custom1.toml", "custom2.toml"])
1653 .unwrap()
1654 .into_string()
1655 .unwrap(),
1656 ),
1657 ..Default::default()
1658 },
1659 wants: vec![
1660 Want::existing("custom1.toml"),
1661 Want::existing("custom2.toml"),
1662 ],
1663 }
1664 }
1665
1666 fn config_path_jj_config_new_multiple() -> TestCase {
1667 TestCase {
1668 files: &["custom1.toml"],
1669 env: UnresolvedConfigEnv {
1670 jj_config: Some(
1671 join_paths(["custom1.toml", "custom2.toml"])
1672 .unwrap()
1673 .into_string()
1674 .unwrap(),
1675 ),
1676 ..Default::default()
1677 },
1678 wants: vec![Want::existing("custom1.toml"), Want::new("custom2.toml")],
1679 }
1680 }
1681
1682 fn config_path_jj_config_empty_paths_filtered() -> TestCase {
1683 TestCase {
1684 files: &["custom1.toml"],
1685 env: UnresolvedConfigEnv {
1686 jj_config: Some(
1687 join_paths(["custom1.toml", "", "custom2.toml"])
1688 .unwrap()
1689 .into_string()
1690 .unwrap(),
1691 ),
1692 ..Default::default()
1693 },
1694 wants: vec![Want::existing("custom1.toml"), Want::new("custom2.toml")],
1695 }
1696 }
1697
1698 fn config_path_jj_config_empty() -> TestCase {
1699 TestCase {
1700 files: &[],
1701 env: UnresolvedConfigEnv {
1702 jj_config: Some("".to_owned()),
1703 ..Default::default()
1704 },
1705 wants: vec![],
1706 }
1707 }
1708
1709 fn config_path_config_pick_platform() -> TestCase {
1710 TestCase {
1711 files: &["config/jj/config.toml"],
1712 env: UnresolvedConfigEnv {
1713 home_dir: Some("home".into()),
1714 config_dir: Some("config".into()),
1715 ..Default::default()
1716 },
1717 wants: vec![Want::existing("config/jj/config.toml")],
1718 }
1719 }
1720
1721 fn config_path_config_pick_home() -> TestCase {
1722 TestCase {
1723 files: &["home/.jjconfig.toml"],
1724 env: UnresolvedConfigEnv {
1725 home_dir: Some("home".into()),
1726 config_dir: Some("config".into()),
1727 ..Default::default()
1728 },
1729 wants: vec![
1730 Want::existing("home/.jjconfig.toml"),
1731 Want::new("config/jj/config.toml"),
1732 ],
1733 }
1734 }
1735
1736 fn config_path_platform_new_conf_dir_existing() -> TestCase {
1737 TestCase {
1738 files: &["config/jj/conf.d/_"],
1739 env: UnresolvedConfigEnv {
1740 home_dir: Some("home".into()),
1741 config_dir: Some("config".into()),
1742 ..Default::default()
1743 },
1744 wants: vec![
1745 Want::new("config/jj/config.toml"),
1746 Want::existing("config/jj/conf.d"),
1747 ],
1748 }
1749 }
1750
1751 fn config_path_platform_existing_conf_dir_existing() -> TestCase {
1752 TestCase {
1753 files: &["config/jj/config.toml", "config/jj/conf.d/_"],
1754 env: UnresolvedConfigEnv {
1755 home_dir: Some("home".into()),
1756 config_dir: Some("config".into()),
1757 ..Default::default()
1758 },
1759 wants: vec![
1760 Want::existing("config/jj/config.toml"),
1761 Want::existing("config/jj/conf.d"),
1762 ],
1763 }
1764 }
1765
1766 fn config_path_all_existing() -> TestCase {
1767 TestCase {
1768 files: &[
1769 "config/jj/conf.d/_",
1770 "config/jj/config.toml",
1771 "home/.jjconfig.toml",
1772 ],
1773 env: UnresolvedConfigEnv {
1774 home_dir: Some("home".into()),
1775 config_dir: Some("config".into()),
1776 ..Default::default()
1777 },
1778 wants: vec![
1780 Want::existing("home/.jjconfig.toml"),
1781 Want::existing("config/jj/config.toml"),
1782 Want::existing("config/jj/conf.d"),
1783 ],
1784 }
1785 }
1786
1787 fn config_path_none() -> TestCase {
1788 TestCase {
1789 files: &[],
1790 env: Default::default(),
1791 wants: vec![],
1792 }
1793 }
1794
1795 #[test_case(config_path_home_existing())]
1796 #[test_case(config_path_home_new())]
1797 #[test_case(config_path_home_existing_platform_new())]
1798 #[test_case(config_path_platform_existing())]
1799 #[test_case(config_path_platform_new())]
1800 #[test_case(config_path_new_prefer_platform())]
1801 #[test_case(config_path_jj_config_existing())]
1802 #[test_case(config_path_jj_config_new())]
1803 #[test_case(config_path_jj_config_existing_multiple())]
1804 #[test_case(config_path_jj_config_new_multiple())]
1805 #[test_case(config_path_jj_config_empty_paths_filtered())]
1806 #[test_case(config_path_jj_config_empty())]
1807 #[test_case(config_path_config_pick_platform())]
1808 #[test_case(config_path_config_pick_home())]
1809 #[test_case(config_path_platform_new_conf_dir_existing())]
1810 #[test_case(config_path_platform_existing_conf_dir_existing())]
1811 #[test_case(config_path_all_existing())]
1812 #[test_case(config_path_none())]
1813 fn test_config_path(case: TestCase) {
1814 let tmp = setup_config_fs(case.files);
1815 let env = resolve_config_env(&case.env, tmp.path());
1816
1817 let all_expected_paths = case
1818 .wants
1819 .iter()
1820 .map(|w| w.rooted_path(tmp.path()))
1821 .collect_vec();
1822 let exists_expected_paths = case
1823 .wants
1824 .iter()
1825 .filter(|w| w.exists())
1826 .map(|w| w.rooted_path(tmp.path()))
1827 .collect_vec();
1828
1829 let all_paths = env.user_config_paths().collect_vec();
1830 let exists_paths = env.existing_user_config_paths().collect_vec();
1831
1832 assert_eq!(all_paths, all_expected_paths);
1833 assert_eq!(exists_paths, exists_expected_paths);
1834 }
1835
1836 fn setup_config_fs(files: &[&str]) -> tempfile::TempDir {
1837 let tmp = testutils::new_temp_dir();
1838 for file in files {
1839 let path = tmp.path().join(file);
1840 if let Some(parent) = path.parent() {
1841 std::fs::create_dir_all(parent).unwrap();
1842 }
1843 std::fs::File::create(path).unwrap();
1844 }
1845 tmp
1846 }
1847
1848 fn resolve_config_env(env: &UnresolvedConfigEnv, root: &Path) -> ConfigEnv {
1849 let home_dir = env.home_dir.as_ref().map(|p| root.join(p));
1850 let env = UnresolvedConfigEnv {
1851 config_dir: env.config_dir.as_ref().map(|p| root.join(p)),
1852 home_dir: home_dir.clone(),
1853 jj_config: env.jj_config.as_ref().map(|p| {
1854 join_paths(split_paths(p).map(|p| {
1855 if p.as_os_str().is_empty() {
1856 return p;
1857 }
1858 root.join(p)
1859 }))
1860 .unwrap()
1861 .into_string()
1862 .unwrap()
1863 }),
1864 };
1865 ConfigEnv {
1866 home_dir,
1867 root_config_dir: None,
1868 repo_path: None,
1869 workspace_path: None,
1870 user_config_paths: env.resolve(),
1871 repo_config: None,
1872 workspace_config: None,
1873 command: None,
1874 hostname: None,
1875 environment: HashMap::new(),
1876 rng: Arc::new(Mutex::new(ChaCha20Rng::seed_from_u64(0))),
1877 }
1878 }
1879}