1use std::path::Path;
18use std::path::PathBuf;
19use std::sync::Arc;
20
21use itertools::Itertools as _;
22use serde::Deserialize as _;
23use serde::de::IntoDeserializer as _;
24use thiserror::Error;
25use toml_edit::DocumentMut;
26
27use crate::config::ConfigGetError;
28use crate::config::ConfigLayer;
29use crate::config::ConfigNamePathBuf;
30use crate::config::ConfigSource;
31use crate::config::ConfigUpdateError;
32use crate::config::ConfigValue;
33use crate::config::StackedConfig;
34use crate::config::ToConfigNamePath;
35
36const SCOPE_CONDITION_KEY: &str = "--when";
39const SCOPE_TABLE_KEY: &str = "--scope";
40
41#[derive(Clone, Debug)]
43pub struct ConfigResolutionContext<'a> {
44 pub home_dir: Option<&'a Path>,
46 pub repo_path: Option<&'a Path>,
48 pub command: Option<&'a str>,
51}
52
53#[derive(Clone, Debug, Default, serde::Deserialize)]
61#[serde(default, rename_all = "kebab-case")]
62struct ScopeCondition {
63 pub repositories: Option<Vec<PathBuf>>,
65 pub commands: Option<Vec<String>>,
70 pub platforms: Option<Vec<String>>,
73 }
75
76impl ScopeCondition {
77 fn from_value(
78 value: ConfigValue,
79 context: &ConfigResolutionContext,
80 ) -> Result<Self, toml_edit::de::Error> {
81 Self::deserialize(value.into_deserializer())?
82 .expand_paths(context)
83 .map_err(serde::de::Error::custom)
84 }
85
86 fn expand_paths(mut self, context: &ConfigResolutionContext) -> Result<Self, &'static str> {
87 for path in self.repositories.as_mut().into_iter().flatten() {
91 if let Some(new_path) = expand_home(path, context.home_dir)? {
92 *path = new_path;
93 }
94 }
95 Ok(self)
96 }
97
98 fn matches(&self, context: &ConfigResolutionContext) -> bool {
99 matches_path_prefix(self.repositories.as_deref(), context.repo_path)
100 && matches_platform(self.platforms.as_deref())
101 && matches_command(self.commands.as_deref(), context.command)
102 }
103}
104
105fn expand_home(path: &Path, home_dir: Option<&Path>) -> Result<Option<PathBuf>, &'static str> {
106 match path.strip_prefix("~") {
107 Ok(tail) => {
108 let home_dir = home_dir.ok_or("Cannot expand ~ (home directory is unknown)")?;
109 Ok(Some(home_dir.join(tail)))
110 }
111 Err(_) => Ok(None),
112 }
113}
114
115fn matches_path_prefix(candidates: Option<&[PathBuf]>, actual: Option<&Path>) -> bool {
116 match (candidates, actual) {
117 (Some(candidates), Some(actual)) => candidates.iter().any(|base| actual.starts_with(base)),
118 (Some(_), None) => false, (None, _) => true, }
121}
122
123fn matches_platform(candidates: Option<&[String]>) -> bool {
124 candidates.is_none_or(|candidates| {
125 candidates
126 .iter()
127 .any(|value| value == std::env::consts::FAMILY || value == std::env::consts::OS)
128 })
129}
130
131fn matches_command(candidates: Option<&[String]>, actual: Option<&str>) -> bool {
132 match (candidates, actual) {
133 (Some(candidates), Some(actual)) => candidates.iter().any(|candidate| {
134 actual
135 .strip_prefix(candidate)
136 .is_some_and(|trailing| trailing.starts_with(' ') || trailing.is_empty())
137 }),
138 (Some(_), None) => false,
139 (None, _) => true,
140 }
141}
142
143pub fn resolve(
146 source_config: &StackedConfig,
147 context: &ConfigResolutionContext,
148) -> Result<StackedConfig, ConfigGetError> {
149 let mut source_layers_stack: Vec<Arc<ConfigLayer>> =
150 source_config.layers().iter().rev().cloned().collect();
151 let mut resolved_layers: Vec<Arc<ConfigLayer>> = Vec::new();
152 while let Some(mut source_layer) = source_layers_stack.pop() {
153 if !source_layer.data.contains_key(SCOPE_CONDITION_KEY)
154 && !source_layer.data.contains_key(SCOPE_TABLE_KEY)
155 {
156 resolved_layers.push(source_layer); continue;
158 }
159
160 let layer_mut = Arc::make_mut(&mut source_layer);
161 let condition = pop_scope_condition(layer_mut, context)?;
162 if !condition.matches(context) {
163 continue;
164 }
165 let tables = pop_scope_tables(layer_mut)?;
166 let frame = source_layers_stack.len();
169 for table in tables {
170 let layer = ConfigLayer {
171 source: source_layer.source,
172 path: source_layer.path.clone(),
173 data: DocumentMut::from(table),
174 };
175 source_layers_stack.push(Arc::new(layer));
176 }
177 source_layers_stack[frame..].reverse();
178 resolved_layers.push(source_layer);
179 }
180 let mut resolved_config = StackedConfig::empty();
181 resolved_config.extend_layers(resolved_layers);
182 Ok(resolved_config)
183}
184
185fn pop_scope_condition(
186 layer: &mut ConfigLayer,
187 context: &ConfigResolutionContext,
188) -> Result<ScopeCondition, ConfigGetError> {
189 let Some(item) = layer.data.remove(SCOPE_CONDITION_KEY) else {
190 return Ok(ScopeCondition::default());
191 };
192 let value = item
193 .clone()
194 .into_value()
195 .expect("Item::None should not exist in table");
196 ScopeCondition::from_value(value, context).map_err(|err| ConfigGetError::Type {
197 name: SCOPE_CONDITION_KEY.to_owned(),
198 error: err.into(),
199 source_path: layer.path.clone(),
200 })
201}
202
203fn pop_scope_tables(layer: &mut ConfigLayer) -> Result<toml_edit::ArrayOfTables, ConfigGetError> {
204 let Some(item) = layer.data.remove(SCOPE_TABLE_KEY) else {
205 return Ok(toml_edit::ArrayOfTables::new());
206 };
207 item.into_array_of_tables()
208 .map_err(|item| ConfigGetError::Type {
209 name: SCOPE_TABLE_KEY.to_owned(),
210 error: format!("Expected an array of tables, but is {}", item.type_name()).into(),
211 source_path: layer.path.clone(),
212 })
213}
214
215#[derive(Debug, Error)]
217#[error("Migration failed")]
218pub struct ConfigMigrateError {
219 #[source]
221 pub error: ConfigMigrateLayerError,
222 pub source_path: Option<PathBuf>,
224}
225
226#[derive(Debug, Error)]
228pub enum ConfigMigrateLayerError {
229 #[error(transparent)]
231 Update(#[from] ConfigUpdateError),
232 #[error("Invalid type or value for {name}")]
234 Type {
235 name: String,
237 #[source]
239 error: DynError,
240 },
241}
242
243impl ConfigMigrateLayerError {
244 fn with_source_path(self, source_path: Option<&Path>) -> ConfigMigrateError {
245 ConfigMigrateError {
246 error: self,
247 source_path: source_path.map(|path| path.to_owned()),
248 }
249 }
250}
251
252type DynError = Box<dyn std::error::Error + Send + Sync>;
253
254pub struct ConfigMigrationRule {
256 inner: MigrationRule,
257}
258
259enum MigrationRule {
260 RenameValue {
261 old_name: ConfigNamePathBuf,
262 new_name: ConfigNamePathBuf,
263 },
264 RenameUpdateValue {
265 old_name: ConfigNamePathBuf,
266 new_name: ConfigNamePathBuf,
267 #[expect(clippy::type_complexity)] new_value_fn: Box<dyn Fn(&ConfigValue) -> Result<ConfigValue, DynError>>,
269 },
270 Custom {
271 matches_fn: Box<dyn Fn(&ConfigLayer) -> bool>,
272 #[expect(clippy::type_complexity)] apply_fn: Box<dyn Fn(&mut ConfigLayer) -> Result<String, ConfigMigrateLayerError>>,
274 },
275}
276
277impl ConfigMigrationRule {
278 pub fn rename_value(old_name: impl ToConfigNamePath, new_name: impl ToConfigNamePath) -> Self {
280 let inner = MigrationRule::RenameValue {
281 old_name: old_name.into_name_path().into(),
282 new_name: new_name.into_name_path().into(),
283 };
284 Self { inner }
285 }
286
287 pub fn rename_update_value(
293 old_name: impl ToConfigNamePath,
294 new_name: impl ToConfigNamePath,
295 new_value_fn: impl Fn(&ConfigValue) -> Result<ConfigValue, DynError> + 'static,
296 ) -> Self {
297 let inner = MigrationRule::RenameUpdateValue {
298 old_name: old_name.into_name_path().into(),
299 new_name: new_name.into_name_path().into(),
300 new_value_fn: Box::new(new_value_fn),
301 };
302 Self { inner }
303 }
304
305 pub fn custom(
310 matches_fn: impl Fn(&ConfigLayer) -> bool + 'static,
311 apply_fn: impl Fn(&mut ConfigLayer) -> Result<String, ConfigMigrateLayerError> + 'static,
312 ) -> Self {
313 let inner = MigrationRule::Custom {
314 matches_fn: Box::new(matches_fn),
315 apply_fn: Box::new(apply_fn),
316 };
317 Self { inner }
318 }
319
320 fn matches(&self, layer: &ConfigLayer) -> bool {
322 match &self.inner {
323 MigrationRule::RenameValue { old_name, .. }
324 | MigrationRule::RenameUpdateValue { old_name, .. } => {
325 matches!(layer.look_up_item(old_name), Ok(Some(_)))
326 }
327 MigrationRule::Custom { matches_fn, .. } => matches_fn(layer),
328 }
329 }
330
331 fn apply(&self, layer: &mut ConfigLayer) -> Result<String, ConfigMigrateLayerError> {
333 match &self.inner {
334 MigrationRule::RenameValue { old_name, new_name } => {
335 rename_value(layer, old_name, new_name)
336 }
337 MigrationRule::RenameUpdateValue {
338 old_name,
339 new_name,
340 new_value_fn,
341 } => rename_update_value(layer, old_name, new_name, new_value_fn),
342 MigrationRule::Custom { apply_fn, .. } => apply_fn(layer),
343 }
344 }
345}
346
347fn rename_value(
348 layer: &mut ConfigLayer,
349 old_name: &ConfigNamePathBuf,
350 new_name: &ConfigNamePathBuf,
351) -> Result<String, ConfigMigrateLayerError> {
352 let value = layer.delete_value(old_name)?.expect("tested by matches()");
353 if matches!(layer.look_up_item(new_name), Ok(Some(_))) {
354 return Ok(format!("{old_name} is deleted (superseded by {new_name})"));
355 }
356 layer.set_value(new_name, value)?;
357 Ok(format!("{old_name} is renamed to {new_name}"))
358}
359
360fn rename_update_value(
361 layer: &mut ConfigLayer,
362 old_name: &ConfigNamePathBuf,
363 new_name: &ConfigNamePathBuf,
364 new_value_fn: impl FnOnce(&ConfigValue) -> Result<ConfigValue, DynError>,
365) -> Result<String, ConfigMigrateLayerError> {
366 let old_value = layer.delete_value(old_name)?.expect("tested by matches()");
367 if matches!(layer.look_up_item(new_name), Ok(Some(_))) {
368 return Ok(format!("{old_name} is deleted (superseded by {new_name})"));
369 }
370 let new_value = new_value_fn(&old_value).map_err(|error| ConfigMigrateLayerError::Type {
371 name: old_name.to_string(),
372 error,
373 })?;
374 layer.set_value(new_name, new_value.clone())?;
375 Ok(format!("{old_name} is updated to {new_name} = {new_value}"))
376}
377
378pub fn migrate(
381 config: &mut StackedConfig,
382 rules: &[ConfigMigrationRule],
383) -> Result<Vec<(ConfigSource, String)>, ConfigMigrateError> {
384 let mut descriptions = Vec::new();
385 for layer in config.layers_mut() {
386 migrate_layer(layer, rules, &mut descriptions)
387 .map_err(|err| err.with_source_path(layer.path.as_deref()))?;
388 }
389 Ok(descriptions)
390}
391
392fn migrate_layer(
393 layer: &mut Arc<ConfigLayer>,
394 rules: &[ConfigMigrationRule],
395 descriptions: &mut Vec<(ConfigSource, String)>,
396) -> Result<(), ConfigMigrateLayerError> {
397 let rules_to_apply = rules
398 .iter()
399 .filter(|rule| rule.matches(layer))
400 .collect_vec();
401 if rules_to_apply.is_empty() {
402 return Ok(());
403 }
404 let layer_mut = Arc::make_mut(layer);
405 for rule in rules_to_apply {
406 let desc = rule.apply(layer_mut)?;
407 descriptions.push((layer_mut.source, desc));
408 }
409 Ok(())
410}
411
412#[cfg(test)]
413mod tests {
414 use assert_matches::assert_matches;
415 use indoc::indoc;
416
417 use super::*;
418 use crate::config::ConfigSource;
419
420 #[test]
421 fn test_expand_home() {
422 let home_dir = Some(Path::new("/home/dir"));
423 assert_eq!(
424 expand_home("~".as_ref(), home_dir).unwrap(),
425 Some(PathBuf::from("/home/dir"))
426 );
427 assert_eq!(expand_home("~foo".as_ref(), home_dir).unwrap(), None);
428 assert_eq!(expand_home("/foo/~".as_ref(), home_dir).unwrap(), None);
429 assert_eq!(
430 expand_home("~/foo".as_ref(), home_dir).unwrap(),
431 Some(PathBuf::from("/home/dir/foo"))
432 );
433 assert!(expand_home("~/foo".as_ref(), None).is_err());
434 }
435
436 #[test]
437 fn test_condition_default() {
438 let condition = ScopeCondition::default();
439
440 let context = ConfigResolutionContext {
441 home_dir: None,
442 repo_path: None,
443 command: None,
444 };
445 assert!(condition.matches(&context));
446 let context = ConfigResolutionContext {
447 home_dir: None,
448 repo_path: Some(Path::new("/foo")),
449 command: None,
450 };
451 assert!(condition.matches(&context));
452 }
453
454 #[test]
455 fn test_condition_repo_path() {
456 let condition = ScopeCondition {
457 repositories: Some(["/foo", "/bar"].map(PathBuf::from).into()),
458 commands: None,
459 platforms: None,
460 };
461
462 let context = ConfigResolutionContext {
463 home_dir: None,
464 repo_path: None,
465 command: None,
466 };
467 assert!(!condition.matches(&context));
468 let context = ConfigResolutionContext {
469 home_dir: None,
470 repo_path: Some(Path::new("/foo")),
471 command: None,
472 };
473 assert!(condition.matches(&context));
474 let context = ConfigResolutionContext {
475 home_dir: None,
476 repo_path: Some(Path::new("/fooo")),
477 command: None,
478 };
479 assert!(!condition.matches(&context));
480 let context = ConfigResolutionContext {
481 home_dir: None,
482 repo_path: Some(Path::new("/foo/baz")),
483 command: None,
484 };
485 assert!(condition.matches(&context));
486 let context = ConfigResolutionContext {
487 home_dir: None,
488 repo_path: Some(Path::new("/bar")),
489 command: None,
490 };
491 assert!(condition.matches(&context));
492 }
493
494 #[test]
495 fn test_condition_repo_path_windows() {
496 let condition = ScopeCondition {
497 repositories: Some(["c:/foo", r"d:\bar/baz"].map(PathBuf::from).into()),
498 commands: None,
499 platforms: None,
500 };
501
502 let context = ConfigResolutionContext {
503 home_dir: None,
504 repo_path: Some(Path::new(r"c:\foo")),
505 command: None,
506 };
507 assert_eq!(condition.matches(&context), cfg!(windows));
508 let context = ConfigResolutionContext {
509 home_dir: None,
510 repo_path: Some(Path::new(r"c:\foo\baz")),
511 command: None,
512 };
513 assert_eq!(condition.matches(&context), cfg!(windows));
514 let context = ConfigResolutionContext {
515 home_dir: None,
516 repo_path: Some(Path::new(r"d:\foo")),
517 command: None,
518 };
519 assert!(!condition.matches(&context));
520 let context = ConfigResolutionContext {
521 home_dir: None,
522 repo_path: Some(Path::new(r"d:/bar\baz")),
523 command: None,
524 };
525 assert_eq!(condition.matches(&context), cfg!(windows));
526 }
527
528 fn new_user_layer(text: &str) -> ConfigLayer {
529 ConfigLayer::parse(ConfigSource::User, text).unwrap()
530 }
531
532 #[test]
533 fn test_resolve_transparent() {
534 let mut source_config = StackedConfig::empty();
535 source_config.add_layer(ConfigLayer::empty(ConfigSource::Default));
536 source_config.add_layer(ConfigLayer::empty(ConfigSource::User));
537
538 let context = ConfigResolutionContext {
539 home_dir: None,
540 repo_path: None,
541 command: None,
542 };
543 let resolved_config = resolve(&source_config, &context).unwrap();
544 assert_eq!(resolved_config.layers().len(), 2);
545 assert!(Arc::ptr_eq(
546 &source_config.layers()[0],
547 &resolved_config.layers()[0]
548 ));
549 assert!(Arc::ptr_eq(
550 &source_config.layers()[1],
551 &resolved_config.layers()[1]
552 ));
553 }
554
555 #[test]
556 fn test_resolve_table_order() {
557 let mut source_config = StackedConfig::empty();
558 source_config.add_layer(new_user_layer(indoc! {"
559 a = 'a #0'
560 [[--scope]]
561 a = 'a #0.0'
562 [[--scope]]
563 a = 'a #0.1'
564 [[--scope.--scope]]
565 a = 'a #0.1.0'
566 [[--scope]]
567 a = 'a #0.2'
568 "}));
569 source_config.add_layer(new_user_layer(indoc! {"
570 a = 'a #1'
571 [[--scope]]
572 a = 'a #1.0'
573 "}));
574
575 let context = ConfigResolutionContext {
576 home_dir: None,
577 repo_path: None,
578 command: None,
579 };
580 let resolved_config = resolve(&source_config, &context).unwrap();
581 assert_eq!(resolved_config.layers().len(), 7);
582 insta::assert_snapshot!(resolved_config.layers()[0].data, @"a = 'a #0'");
583 insta::assert_snapshot!(resolved_config.layers()[1].data, @"a = 'a #0.0'");
584 insta::assert_snapshot!(resolved_config.layers()[2].data, @"a = 'a #0.1'");
585 insta::assert_snapshot!(resolved_config.layers()[3].data, @"a = 'a #0.1.0'");
586 insta::assert_snapshot!(resolved_config.layers()[4].data, @"a = 'a #0.2'");
587 insta::assert_snapshot!(resolved_config.layers()[5].data, @"a = 'a #1'");
588 insta::assert_snapshot!(resolved_config.layers()[6].data, @"a = 'a #1.0'");
589 }
590
591 #[test]
592 fn test_resolve_repo_path() {
593 let mut source_config = StackedConfig::empty();
594 source_config.add_layer(new_user_layer(indoc! {"
595 a = 'a #0'
596 [[--scope]]
597 --when.repositories = ['/foo']
598 a = 'a #0.1 foo'
599 [[--scope]]
600 --when.repositories = ['/foo', '/bar']
601 a = 'a #0.2 foo|bar'
602 [[--scope]]
603 --when.repositories = []
604 a = 'a #0.3 none'
605 "}));
606 source_config.add_layer(new_user_layer(indoc! {"
607 --when.repositories = ['~/baz']
608 a = 'a #1 baz'
609 [[--scope]]
610 --when.repositories = ['/foo'] # should never be enabled
611 a = 'a #1.1 baz&foo'
612 "}));
613
614 let context = ConfigResolutionContext {
615 home_dir: Some(Path::new("/home/dir")),
616 repo_path: None,
617 command: None,
618 };
619 let resolved_config = resolve(&source_config, &context).unwrap();
620 assert_eq!(resolved_config.layers().len(), 1);
621 insta::assert_snapshot!(resolved_config.layers()[0].data, @"a = 'a #0'");
622
623 let context = ConfigResolutionContext {
624 home_dir: Some(Path::new("/home/dir")),
625 repo_path: Some(Path::new("/foo/.jj/repo")),
626 command: None,
627 };
628 let resolved_config = resolve(&source_config, &context).unwrap();
629 assert_eq!(resolved_config.layers().len(), 3);
630 insta::assert_snapshot!(resolved_config.layers()[0].data, @"a = 'a #0'");
631 insta::assert_snapshot!(resolved_config.layers()[1].data, @"a = 'a #0.1 foo'");
632 insta::assert_snapshot!(resolved_config.layers()[2].data, @"a = 'a #0.2 foo|bar'");
633
634 let context = ConfigResolutionContext {
635 home_dir: Some(Path::new("/home/dir")),
636 repo_path: Some(Path::new("/bar/.jj/repo")),
637 command: None,
638 };
639 let resolved_config = resolve(&source_config, &context).unwrap();
640 assert_eq!(resolved_config.layers().len(), 2);
641 insta::assert_snapshot!(resolved_config.layers()[0].data, @"a = 'a #0'");
642 insta::assert_snapshot!(resolved_config.layers()[1].data, @"a = 'a #0.2 foo|bar'");
643
644 let context = ConfigResolutionContext {
645 home_dir: Some(Path::new("/home/dir")),
646 repo_path: Some(Path::new("/home/dir/baz/.jj/repo")),
647 command: None,
648 };
649 let resolved_config = resolve(&source_config, &context).unwrap();
650 assert_eq!(resolved_config.layers().len(), 2);
651 insta::assert_snapshot!(resolved_config.layers()[0].data, @"a = 'a #0'");
652 insta::assert_snapshot!(resolved_config.layers()[1].data, @"a = 'a #1 baz'");
653 }
654
655 #[test]
656 fn test_resolve_command() {
657 let mut source_config = StackedConfig::empty();
658 source_config.add_layer(new_user_layer(indoc! {"
659 a = 'a #0'
660 [[--scope]]
661 --when.commands = ['foo']
662 a = 'a #0.1 foo'
663 [[--scope]]
664 --when.commands = ['foo', 'bar']
665 a = 'a #0.2 foo|bar'
666 [[--scope]]
667 --when.commands = ['foo baz']
668 a = 'a #0.3 foo baz'
669 [[--scope]]
670 --when.commands = []
671 a = 'a #0.4 none'
672 "}));
673
674 let context = ConfigResolutionContext {
675 home_dir: None,
676 repo_path: None,
677 command: None,
678 };
679 let resolved_config = resolve(&source_config, &context).unwrap();
680 assert_eq!(resolved_config.layers().len(), 1);
681 insta::assert_snapshot!(resolved_config.layers()[0].data, @"a = 'a #0'");
682
683 let context = ConfigResolutionContext {
684 home_dir: None,
685 repo_path: None,
686 command: Some("foo"),
687 };
688 let resolved_config = resolve(&source_config, &context).unwrap();
689 assert_eq!(resolved_config.layers().len(), 3);
690 insta::assert_snapshot!(resolved_config.layers()[0].data, @"a = 'a #0'");
691 insta::assert_snapshot!(resolved_config.layers()[1].data, @"a = 'a #0.1 foo'");
692 insta::assert_snapshot!(resolved_config.layers()[2].data, @"a = 'a #0.2 foo|bar'");
693
694 let context = ConfigResolutionContext {
695 home_dir: None,
696 repo_path: None,
697 command: Some("bar"),
698 };
699 let resolved_config = resolve(&source_config, &context).unwrap();
700 assert_eq!(resolved_config.layers().len(), 2);
701 insta::assert_snapshot!(resolved_config.layers()[0].data, @"a = 'a #0'");
702 insta::assert_snapshot!(resolved_config.layers()[1].data, @"a = 'a #0.2 foo|bar'");
703
704 let context = ConfigResolutionContext {
705 home_dir: None,
706 repo_path: None,
707 command: Some("foo baz"),
708 };
709 let resolved_config = resolve(&source_config, &context).unwrap();
710 assert_eq!(resolved_config.layers().len(), 4);
711 insta::assert_snapshot!(resolved_config.layers()[0].data, @"a = 'a #0'");
712 insta::assert_snapshot!(resolved_config.layers()[1].data, @"a = 'a #0.1 foo'");
713 insta::assert_snapshot!(resolved_config.layers()[2].data, @"a = 'a #0.2 foo|bar'");
714 insta::assert_snapshot!(resolved_config.layers()[3].data, @"a = 'a #0.3 foo baz'");
715
716 let context = ConfigResolutionContext {
718 home_dir: None,
719 repo_path: None,
720 command: Some("fooqux"),
721 };
722 let resolved_config = resolve(&source_config, &context).unwrap();
723 assert_eq!(resolved_config.layers().len(), 1);
724 insta::assert_snapshot!(resolved_config.layers()[0].data, @"a = 'a #0'");
725 }
726
727 #[test]
728 fn test_resolve_os() {
729 let mut source_config = StackedConfig::empty();
730 source_config.add_layer(new_user_layer(indoc! {"
731 a = 'a none'
732 b = 'b none'
733 [[--scope]]
734 --when.platforms = ['linux']
735 a = 'a linux'
736 [[--scope]]
737 --when.platforms = ['macos']
738 a = 'a macos'
739 [[--scope]]
740 --when.platforms = ['windows']
741 a = 'a windows'
742 [[--scope]]
743 --when.platforms = ['unix']
744 b = 'b unix'
745 "}));
746
747 let context = ConfigResolutionContext {
748 home_dir: Some(Path::new("/home/dir")),
749 repo_path: None,
750 command: None,
751 };
752 let resolved_config = resolve(&source_config, &context).unwrap();
753 insta::assert_snapshot!(resolved_config.layers()[0].data, @r#"
754 a = 'a none'
755 b = 'b none'
756 "#);
757 if cfg!(target_os = "linux") {
758 assert_eq!(resolved_config.layers().len(), 3);
759 insta::assert_snapshot!(resolved_config.layers()[1].data, @"a = 'a linux'");
760 insta::assert_snapshot!(resolved_config.layers()[2].data, @"b = 'b unix'");
761 } else if cfg!(target_os = "macos") {
762 assert_eq!(resolved_config.layers().len(), 3);
763 insta::assert_snapshot!(resolved_config.layers()[1].data, @"a = 'a macos'");
764 insta::assert_snapshot!(resolved_config.layers()[2].data, @"b = 'b unix'");
765 } else if cfg!(target_os = "windows") {
766 assert_eq!(resolved_config.layers().len(), 2);
767 insta::assert_snapshot!(resolved_config.layers()[1].data, @"a = 'a windows'");
768 } else {
769 assert_eq!(resolved_config.layers().len(), 1);
770 }
771 }
772
773 #[test]
774 fn test_resolve_repo_path_and_command() {
775 let mut source_config = StackedConfig::empty();
776 source_config.add_layer(new_user_layer(indoc! {"
777 a = 'a #0'
778 [[--scope]]
779 --when.repositories = ['/foo', '/bar']
780 --when.commands = ['ABC', 'DEF']
781 a = 'a #0.1'
782 "}));
783
784 let context = ConfigResolutionContext {
785 home_dir: Some(Path::new("/home/dir")),
786 repo_path: None,
787 command: None,
788 };
789 let resolved_config = resolve(&source_config, &context).unwrap();
790 assert_eq!(resolved_config.layers().len(), 1);
791 insta::assert_snapshot!(resolved_config.layers()[0].data, @"a = 'a #0'");
792
793 let context = ConfigResolutionContext {
795 home_dir: Some(Path::new("/home/dir")),
796 repo_path: Some(Path::new("/foo")),
797 command: Some("other"),
798 };
799 let resolved_config = resolve(&source_config, &context).unwrap();
800 assert_eq!(resolved_config.layers().len(), 1);
801 insta::assert_snapshot!(resolved_config.layers()[0].data, @"a = 'a #0'");
802
803 let context = ConfigResolutionContext {
805 home_dir: Some(Path::new("/home/dir")),
806 repo_path: Some(Path::new("/qux")),
807 command: Some("ABC"),
808 };
809 let resolved_config = resolve(&source_config, &context).unwrap();
810 assert_eq!(resolved_config.layers().len(), 1);
811 insta::assert_snapshot!(resolved_config.layers()[0].data, @"a = 'a #0'");
812
813 let context = ConfigResolutionContext {
815 home_dir: Some(Path::new("/home/dir")),
816 repo_path: Some(Path::new("/bar")),
817 command: Some("DEF"),
818 };
819 let resolved_config = resolve(&source_config, &context).unwrap();
820 assert_eq!(resolved_config.layers().len(), 2);
821 insta::assert_snapshot!(resolved_config.layers()[0].data, @"a = 'a #0'");
822 insta::assert_snapshot!(resolved_config.layers()[1].data, @"a = 'a #0.1'");
823 }
824
825 #[test]
826 fn test_resolve_invalid_condition() {
827 let new_config = |text: &str| {
828 let mut config = StackedConfig::empty();
829 config.add_layer(new_user_layer(text));
830 config
831 };
832 let context = ConfigResolutionContext {
833 home_dir: Some(Path::new("/home/dir")),
834 repo_path: Some(Path::new("/foo/.jj/repo")),
835 command: None,
836 };
837 assert_matches!(
838 resolve(&new_config("--when.repositories = 0"), &context),
839 Err(ConfigGetError::Type { .. })
840 );
841 }
842
843 #[test]
844 fn test_resolve_invalid_scoped_tables() {
845 let new_config = |text: &str| {
846 let mut config = StackedConfig::empty();
847 config.add_layer(new_user_layer(text));
848 config
849 };
850 let context = ConfigResolutionContext {
851 home_dir: Some(Path::new("/home/dir")),
852 repo_path: Some(Path::new("/foo/.jj/repo")),
853 command: None,
854 };
855 assert_matches!(
856 resolve(&new_config("[--scope]"), &context),
857 Err(ConfigGetError::Type { .. })
858 );
859 }
860
861 #[test]
862 fn test_migrate_noop() {
863 let mut config = StackedConfig::empty();
864 config.add_layer(new_user_layer(indoc! {"
865 foo = 'foo'
866 "}));
867 config.add_layer(new_user_layer(indoc! {"
868 bar = 'bar'
869 "}));
870
871 let old_layers = config.layers().to_vec();
872 let rules = [ConfigMigrationRule::rename_value("baz", "foo")];
873 let descriptions = migrate(&mut config, &rules).unwrap();
874 assert!(descriptions.is_empty());
875 assert!(Arc::ptr_eq(&config.layers()[0], &old_layers[0]));
876 assert!(Arc::ptr_eq(&config.layers()[1], &old_layers[1]));
877 }
878
879 #[test]
880 fn test_migrate_error() {
881 let mut config = StackedConfig::empty();
882 let mut layer = new_user_layer(indoc! {"
883 foo.bar = 'baz'
884 "});
885 layer.path = Some("source.toml".into());
886 config.add_layer(layer);
887
888 let rules = [ConfigMigrationRule::rename_value("foo", "bar")];
889 insta::assert_debug_snapshot!(migrate(&mut config, &rules).unwrap_err(), @r#"
890 ConfigMigrateError {
891 error: Update(
892 WouldDeleteTable {
893 name: "foo",
894 },
895 ),
896 source_path: Some(
897 "source.toml",
898 ),
899 }
900 "#);
901 }
902
903 #[test]
904 fn test_migrate_rename_value() {
905 let mut config = StackedConfig::empty();
906 config.add_layer(new_user_layer(indoc! {"
907 [foo]
908 old = 'foo.old #0'
909 [bar]
910 old = 'bar.old #0'
911 [baz]
912 new = 'baz.new #0'
913 "}));
914 config.add_layer(new_user_layer(indoc! {"
915 [bar]
916 old = 'bar.old #1'
917 "}));
918
919 let rules = [
920 ConfigMigrationRule::rename_value("foo.old", "foo.new"),
921 ConfigMigrationRule::rename_value("bar.old", "baz.new"),
922 ];
923 let descriptions = migrate(&mut config, &rules).unwrap();
924 insta::assert_debug_snapshot!(descriptions, @r#"
925 [
926 (
927 User,
928 "foo.old is renamed to foo.new",
929 ),
930 (
931 User,
932 "bar.old is deleted (superseded by baz.new)",
933 ),
934 (
935 User,
936 "bar.old is renamed to baz.new",
937 ),
938 ]
939 "#);
940 insta::assert_snapshot!(config.layers()[0].data, @r"
941 [foo]
942 new = 'foo.old #0'
943 [bar]
944 [baz]
945 new = 'baz.new #0'
946 ");
947 insta::assert_snapshot!(config.layers()[1].data, @r"
948 [bar]
949
950 [baz]
951 new = 'bar.old #1'
952 ");
953 }
954
955 #[test]
956 fn test_migrate_rename_update_value() {
957 let mut config = StackedConfig::empty();
958 config.add_layer(new_user_layer(indoc! {"
959 [foo]
960 old = 'foo.old #0'
961 [bar]
962 old = 'bar.old #0'
963 [baz]
964 new = 'baz.new #0'
965 "}));
966 config.add_layer(new_user_layer(indoc! {"
967 [bar]
968 old = 'bar.old #1'
969 "}));
970
971 let rules = [
972 ConfigMigrationRule::rename_update_value("foo.old", "foo.new", |old_value| {
974 let val = old_value.clone().decorated("", "");
975 Ok(ConfigValue::from_iter([val]))
976 }),
977 ConfigMigrationRule::rename_update_value("bar.old", "baz.new", |old_value| {
979 let s = old_value.as_str().ok_or("not a string")?;
980 Ok(format!("{s} updated").into())
981 }),
982 ];
983 let descriptions = migrate(&mut config, &rules).unwrap();
984 insta::assert_debug_snapshot!(descriptions, @r#"
985 [
986 (
987 User,
988 "foo.old is updated to foo.new = ['foo.old #0']",
989 ),
990 (
991 User,
992 "bar.old is deleted (superseded by baz.new)",
993 ),
994 (
995 User,
996 "bar.old is updated to baz.new = \"bar.old #1 updated\"",
997 ),
998 ]
999 "#);
1000 insta::assert_snapshot!(config.layers()[0].data, @r"
1001 [foo]
1002 new = ['foo.old #0']
1003 [bar]
1004 [baz]
1005 new = 'baz.new #0'
1006 ");
1007 insta::assert_snapshot!(config.layers()[1].data, @r#"
1008 [bar]
1009
1010 [baz]
1011 new = "bar.old #1 updated"
1012 "#);
1013
1014 config.add_layer(new_user_layer(indoc! {"
1015 [bar]
1016 old = false # not a string
1017 "}));
1018 insta::assert_debug_snapshot!(migrate(&mut config, &rules).unwrap_err(), @r#"
1019 ConfigMigrateError {
1020 error: Type {
1021 name: "bar.old",
1022 error: "not a string",
1023 },
1024 source_path: None,
1025 }
1026 "#);
1027 }
1028}