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