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