just playing with tangled
1// Copyright 2022 The Jujutsu Authors
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// https://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use std::env::join_paths;
16use std::path::PathBuf;
17
18use indoc::indoc;
19use regex::Regex;
20
21use crate::common::fake_editor_path;
22use crate::common::to_toml_value;
23use crate::common::TestEnvironment;
24
25#[test]
26fn test_config_list_single() {
27 let test_env = TestEnvironment::default();
28 test_env.add_config(
29 r#"
30 [test-table]
31 somekey = "some value"
32 "#,
33 );
34
35 let output = test_env.run_jj_in(".", ["config", "list", "test-table.somekey"]);
36 insta::assert_snapshot!(output, @r#"
37 test-table.somekey = "some value"
38 [EOF]
39 "#);
40
41 let output = test_env.run_jj_in(
42 ".",
43 ["config", "list", r#"-Tname ++ "\n""#, "test-table.somekey"],
44 );
45 insta::assert_snapshot!(output, @r"
46 test-table.somekey
47 [EOF]
48 ");
49}
50
51#[test]
52fn test_config_list_nonexistent() {
53 let test_env = TestEnvironment::default();
54 let output = test_env.run_jj_in(".", ["config", "list", "nonexistent-test-key"]);
55 insta::assert_snapshot!(output, @r"
56 ------- stderr -------
57 Warning: No matching config key for nonexistent-test-key
58 [EOF]
59 ");
60}
61
62#[test]
63fn test_config_list_table() {
64 let test_env = TestEnvironment::default();
65 test_env.add_config(
66 r#"
67 [test-table]
68 x = true
69 y.foo = "abc"
70 y.bar = 123
71 "z"."with space"."function()" = 5
72 "#,
73 );
74 let output = test_env.run_jj_in(".", ["config", "list", "test-table"]);
75 insta::assert_snapshot!(output, @r#"
76 test-table.x = true
77 test-table.y.foo = "abc"
78 test-table.y.bar = 123
79 test-table.z."with space"."function()" = 5
80 [EOF]
81 "#);
82}
83
84#[test]
85fn test_config_list_inline_table() {
86 let test_env = TestEnvironment::default();
87 test_env.add_config(
88 r#"
89 test-table = { x = true, y = 1 }
90 "#,
91 );
92 // Inline tables are expanded
93 let output = test_env.run_jj_in(".", ["config", "list", "test-table"]);
94 insta::assert_snapshot!(output, @r"
95 test-table.x = true
96 test-table.y = 1
97 [EOF]
98 ");
99 // Inner value can also be addressed by a dotted name path
100 let output = test_env.run_jj_in(".", ["config", "list", "test-table.x"]);
101 insta::assert_snapshot!(output, @r"
102 test-table.x = true
103 [EOF]
104 ");
105}
106
107#[test]
108fn test_config_list_array() {
109 let test_env = TestEnvironment::default();
110 test_env.add_config(
111 r#"
112 test-array = [1, "b", 3.4]
113 "#,
114 );
115 let output = test_env.run_jj_in(".", ["config", "list", "test-array"]);
116 insta::assert_snapshot!(output, @r#"
117 test-array = [1, "b", 3.4]
118 [EOF]
119 "#);
120}
121
122#[test]
123fn test_config_list_array_of_tables() {
124 let test_env = TestEnvironment::default();
125 test_env.add_config(
126 r#"
127 [[test-table]]
128 x = 1
129 [[test-table]]
130 y = ["z"]
131 z."key=with whitespace" = []
132 "#,
133 );
134 // Array is a value, so is array of tables
135 let output = test_env.run_jj_in(".", ["config", "list", "test-table"]);
136 insta::assert_snapshot!(output, @r#"
137 test-table = [{ x = 1 }, { y = ["z"], z = { "key=with whitespace" = [] } }]
138 [EOF]
139 "#);
140}
141
142#[test]
143fn test_config_list_all() {
144 let test_env = TestEnvironment::default();
145 test_env.add_config(
146 r#"
147 test-val = [1, 2, 3]
148 [test-table]
149 x = true
150 y.foo = "abc"
151 y.bar = 123
152 "#,
153 );
154
155 let output = test_env.run_jj_in(".", ["config", "list"]);
156 insta::assert_snapshot!(
157 output.normalize_stdout_with(|s| find_stdout_lines(r"(test-val|test-table\b[^=]*)", &s)), @r#"
158 test-val = [1, 2, 3]
159 test-table.x = true
160 test-table.y.foo = "abc"
161 test-table.y.bar = 123
162 [EOF]
163 "#);
164}
165
166#[test]
167fn test_config_list_multiline_string() {
168 let test_env = TestEnvironment::default();
169 test_env.add_config(
170 r#"
171 multiline = '''
172foo
173bar
174'''
175 "#,
176 );
177
178 let output = test_env.run_jj_in(".", ["config", "list", "multiline"]);
179 insta::assert_snapshot!(output, @r"
180 multiline = '''
181 foo
182 bar
183 '''
184 [EOF]
185 ");
186
187 let output = test_env.run_jj_in(
188 ".",
189 [
190 "config",
191 "list",
192 "multiline",
193 "--include-overridden",
194 "--config=multiline='single'",
195 ],
196 );
197 insta::assert_snapshot!(output, @r"
198 # multiline = '''
199 # foo
200 # bar
201 # '''
202 multiline = 'single'
203 [EOF]
204 ");
205}
206
207#[test]
208fn test_config_list_layer() {
209 let mut test_env = TestEnvironment::default();
210 test_env.run_jj_in(".", ["git", "init", "repo"]).success();
211 // Test with fresh new config file
212 let user_config_path = test_env.config_path().join("config.toml");
213 test_env.set_config_path(&user_config_path);
214 let work_dir = test_env.work_dir("repo");
215
216 // User
217 work_dir
218 .run_jj(["config", "set", "--user", "test-key", "test-val"])
219 .success();
220
221 work_dir
222 .run_jj([
223 "config",
224 "set",
225 "--user",
226 "test-layered-key",
227 "test-original-val",
228 ])
229 .success();
230
231 let output = work_dir.run_jj(["config", "list", "--user"]);
232 insta::assert_snapshot!(output, @r#"
233 test-key = "test-val"
234 test-layered-key = "test-original-val"
235 [EOF]
236 "#);
237
238 // Repo
239 work_dir
240 .run_jj([
241 "config",
242 "set",
243 "--repo",
244 "test-layered-key",
245 "test-layered-val",
246 ])
247 .success();
248
249 let output = work_dir.run_jj(["config", "list", "--user"]);
250 insta::assert_snapshot!(output, @r#"
251 test-key = "test-val"
252 [EOF]
253 "#);
254
255 let output = work_dir.run_jj(["config", "list", "--repo"]);
256 insta::assert_snapshot!(output, @r#"
257 test-layered-key = "test-layered-val"
258 [EOF]
259 "#);
260}
261
262#[test]
263fn test_config_list_origin() {
264 let mut test_env = TestEnvironment::default();
265 test_env.run_jj_in(".", ["git", "init", "repo"]).success();
266 // Test with fresh new config file
267 let user_config_path = test_env.config_path().join("config.toml");
268 test_env.set_config_path(&user_config_path);
269 let work_dir = test_env.work_dir("repo");
270
271 // User
272 work_dir
273 .run_jj(["config", "set", "--user", "test-key", "test-val"])
274 .success();
275
276 work_dir
277 .run_jj([
278 "config",
279 "set",
280 "--user",
281 "test-layered-key",
282 "test-original-val",
283 ])
284 .success();
285
286 // Repo
287 work_dir
288 .run_jj([
289 "config",
290 "set",
291 "--repo",
292 "test-layered-key",
293 "test-layered-val",
294 ])
295 .success();
296
297 let output = work_dir.run_jj([
298 "config",
299 "list",
300 "-Tbuiltin_config_list_detailed",
301 "--config",
302 "test-cli-key=test-cli-val",
303 ]);
304 insta::assert_snapshot!(output, @r#"
305 test-key = "test-val" # user $TEST_ENV/config/config.toml
306 test-layered-key = "test-layered-val" # repo $TEST_ENV/repo/.jj/repo/config.toml
307 user.name = "Test User" # env
308 user.email = "test.user@example.com" # env
309 debug.commit-timestamp = "2001-02-03T04:05:11+07:00" # env
310 debug.randomness-seed = 5 # env
311 debug.operation-timestamp = "2001-02-03T04:05:11+07:00" # env
312 operation.hostname = "host.example.com" # env
313 operation.username = "test-username" # env
314 test-cli-key = "test-cli-val" # cli
315 [EOF]
316 "#);
317
318 let output = work_dir.run_jj([
319 "config",
320 "list",
321 "-Tbuiltin_config_list_detailed",
322 "--color=debug",
323 "--include-defaults",
324 "--include-overridden",
325 "--config=test-key=test-cli-val",
326 "test-key",
327 ]);
328 insta::assert_snapshot!(output, @r#"
329 [38;5;8m<<config_list overridden name::# test-key>><<config_list overridden:: = >><<config_list overridden value::"test-val">><<config_list overridden:: # >><<config_list overridden source::user>><<config_list overridden:: >><<config_list overridden path::$TEST_ENV/config/config.toml>><<config_list overridden::>>[39m
330 [38;5;2m<<config_list name::test-key>>[39m<<config_list:: = >>[38;5;3m<<config_list value::"test-cli-val">>[39m<<config_list:: # >>[38;5;4m<<config_list source::cli>>[39m<<config_list::>>
331 [EOF]
332 "#);
333}
334
335#[test]
336fn test_config_layer_override_default() {
337 let test_env = TestEnvironment::default();
338 test_env.run_jj_in(".", ["git", "init", "repo"]).success();
339 let work_dir = test_env.work_dir("repo");
340 let config_key = "merge-tools.vimdiff.program";
341
342 // Default
343 let output = work_dir.run_jj(["config", "list", config_key, "--include-defaults"]);
344 insta::assert_snapshot!(output, @r#"
345 merge-tools.vimdiff.program = "vim"
346 [EOF]
347 "#);
348
349 // User
350 test_env.add_config(format!(
351 "{config_key} = {value}\n",
352 value = to_toml_value("user")
353 ));
354 let output = work_dir.run_jj(["config", "list", config_key]);
355 insta::assert_snapshot!(output, @r#"
356 merge-tools.vimdiff.program = "user"
357 [EOF]
358 "#);
359
360 // Repo
361 work_dir.write_file(
362 ".jj/repo/config.toml",
363 format!("{config_key} = {value}\n", value = to_toml_value("repo")),
364 );
365 let output = work_dir.run_jj(["config", "list", config_key]);
366 insta::assert_snapshot!(output, @r#"
367 merge-tools.vimdiff.program = "repo"
368 [EOF]
369 "#);
370
371 // Command argument
372 let output = work_dir.run_jj([
373 "config",
374 "list",
375 config_key,
376 "--config",
377 &format!("{config_key}={value}", value = to_toml_value("command-arg")),
378 ]);
379 insta::assert_snapshot!(output, @r#"
380 merge-tools.vimdiff.program = "command-arg"
381 [EOF]
382 "#);
383
384 // Allow printing overridden values
385 let output = work_dir.run_jj([
386 "config",
387 "list",
388 config_key,
389 "--include-overridden",
390 "--config",
391 &format!("{config_key}={value}", value = to_toml_value("command-arg")),
392 ]);
393 insta::assert_snapshot!(output, @r##"
394 # merge-tools.vimdiff.program = "user"
395 # merge-tools.vimdiff.program = "repo"
396 merge-tools.vimdiff.program = "command-arg"
397 [EOF]
398 "##);
399
400 let output = work_dir.run_jj([
401 "config",
402 "list",
403 "--color=always",
404 config_key,
405 "--include-overridden",
406 ]);
407 insta::assert_snapshot!(output, @r#"
408 [38;5;8m# merge-tools.vimdiff.program = "user"[39m
409 [38;5;2mmerge-tools.vimdiff.program[39m = [38;5;3m"repo"[39m
410 [EOF]
411 "#);
412}
413
414#[test]
415fn test_config_layer_override_env() {
416 let mut test_env = TestEnvironment::default();
417 test_env.run_jj_in(".", ["git", "init", "repo"]).success();
418 let config_key = "ui.editor";
419
420 // Environment base
421 test_env.add_env_var("EDITOR", "env-base");
422 let work_dir = test_env.work_dir("repo");
423 let output = work_dir.run_jj(["config", "list", config_key]);
424 insta::assert_snapshot!(output, @r#"
425 ui.editor = "env-base"
426 [EOF]
427 "#);
428
429 // User
430 test_env.add_config(format!(
431 "{config_key} = {value}\n",
432 value = to_toml_value("user")
433 ));
434 let output = work_dir.run_jj(["config", "list", config_key]);
435 insta::assert_snapshot!(output, @r#"
436 ui.editor = "user"
437 [EOF]
438 "#);
439
440 // Repo
441 work_dir.write_file(
442 ".jj/repo/config.toml",
443 format!("{config_key} = {value}\n", value = to_toml_value("repo")),
444 );
445 let output = work_dir.run_jj(["config", "list", config_key]);
446 insta::assert_snapshot!(output, @r#"
447 ui.editor = "repo"
448 [EOF]
449 "#);
450
451 // Environment override
452 test_env.add_env_var("JJ_EDITOR", "env-override");
453 let work_dir = test_env.work_dir("repo");
454 let output = work_dir.run_jj(["config", "list", config_key]);
455 insta::assert_snapshot!(output, @r#"
456 ui.editor = "env-override"
457 [EOF]
458 "#);
459
460 // Command argument
461 let output = work_dir.run_jj([
462 "config",
463 "list",
464 config_key,
465 "--config",
466 &format!("{config_key}={value}", value = to_toml_value("command-arg")),
467 ]);
468 insta::assert_snapshot!(output, @r#"
469 ui.editor = "command-arg"
470 [EOF]
471 "#);
472
473 // Allow printing overridden values
474 let output = work_dir.run_jj([
475 "config",
476 "list",
477 config_key,
478 "--include-overridden",
479 "--config",
480 &format!("{config_key}={value}", value = to_toml_value("command-arg")),
481 ]);
482 insta::assert_snapshot!(output, @r##"
483 # ui.editor = "env-base"
484 # ui.editor = "user"
485 # ui.editor = "repo"
486 # ui.editor = "env-override"
487 ui.editor = "command-arg"
488 [EOF]
489 "##);
490}
491
492#[test]
493fn test_config_layer_workspace() {
494 let test_env = TestEnvironment::default();
495 test_env.run_jj_in(".", ["git", "init", "main"]).success();
496 let main_dir = test_env.work_dir("main");
497 let secondary_dir = test_env.work_dir("secondary");
498 let config_key = "ui.editor";
499
500 main_dir.write_file("file", "contents");
501 main_dir.run_jj(["new"]).success();
502 main_dir
503 .run_jj(["workspace", "add", "--name", "second", "../secondary"])
504 .success();
505
506 // Repo
507 main_dir.write_file(
508 ".jj/repo/config.toml",
509 format!(
510 "{config_key} = {value}\n",
511 value = to_toml_value("main-repo")
512 ),
513 );
514 let output = main_dir.run_jj(["config", "list", config_key]);
515 insta::assert_snapshot!(output, @r#"
516 ui.editor = "main-repo"
517 [EOF]
518 "#);
519 let output = secondary_dir.run_jj(["config", "list", config_key]);
520 insta::assert_snapshot!(output, @r#"
521 ui.editor = "main-repo"
522 [EOF]
523 "#);
524}
525
526#[test]
527fn test_config_set_bad_opts() {
528 let test_env = TestEnvironment::default();
529 let output = test_env.run_jj_in(".", ["config", "set"]);
530 insta::assert_snapshot!(output, @r"
531 ------- stderr -------
532 error: the following required arguments were not provided:
533 <--user|--repo>
534 <NAME>
535 <VALUE>
536
537 Usage: jj config set <--user|--repo> <NAME> <VALUE>
538
539 For more information, try '--help'.
540 [EOF]
541 [exit status: 2]
542 ");
543
544 let output = test_env.run_jj_in(".", ["config", "set", "--user", "", "x"]);
545 insta::assert_snapshot!(output, @r"
546 ------- stderr -------
547 error: invalid value '' for '<NAME>': TOML parse error at line 1, column 1
548 |
549 1 |
550 | ^
551 invalid key
552
553
554 For more information, try '--help'.
555 [EOF]
556 [exit status: 2]
557 ");
558
559 let output = test_env.run_jj_in(".", ["config", "set", "--user", "x", "['typo'}"]);
560 insta::assert_snapshot!(output, @r"
561 ------- stderr -------
562 error: invalid value '['typo'}' for '<VALUE>': TOML parse error at line 1, column 8
563 |
564 1 | ['typo'}
565 | ^
566 invalid array
567 expected `]`
568
569
570 For more information, try '--help'.
571 [EOF]
572 [exit status: 2]
573 ");
574}
575
576#[test]
577fn test_config_set_for_user() {
578 let mut test_env = TestEnvironment::default();
579 test_env.run_jj_in(".", ["git", "init", "repo"]).success();
580 // Test with fresh new config file
581 let user_config_path = test_env.config_path().join("config.toml");
582 test_env.set_config_path(&user_config_path);
583 let work_dir = test_env.work_dir("repo");
584
585 work_dir
586 .run_jj(["config", "set", "--user", "test-key", "test-val"])
587 .success();
588 work_dir
589 .run_jj(["config", "set", "--user", "test-table.foo", "true"])
590 .success();
591 work_dir
592 .run_jj(["config", "set", "--user", "test-table.'bar()'", "0"])
593 .success();
594
595 // Ensure test-key successfully written to user config.
596 let user_config_toml = std::fs::read_to_string(&user_config_path)
597 .unwrap_or_else(|_| panic!("Failed to read file {}", user_config_path.display()));
598 insta::assert_snapshot!(user_config_toml, @r#"
599 test-key = "test-val"
600
601 [test-table]
602 foo = true
603 'bar()' = 0
604 "#);
605}
606
607#[test]
608fn test_config_set_for_user_directory() {
609 let test_env = TestEnvironment::default();
610
611 test_env
612 .run_jj_in(".", ["config", "set", "--user", "test-key", "test-val"])
613 .success();
614 insta::assert_snapshot!(
615 std::fs::read_to_string(test_env.last_config_file_path()).unwrap(),
616 @r#"
617 test-key = "test-val"
618
619 [template-aliases]
620 'format_time_range(time_range)' = 'time_range.start() ++ " - " ++ time_range.end()'
621 "#);
622
623 // Add one more config file to the directory
624 test_env.add_config("");
625 let output = test_env.run_jj_in(
626 ".",
627 ["config", "set", "--user", "test-key", "test-other-val"],
628 );
629 insta::assert_snapshot!(output, @r"
630 ------- stderr -------
631 1: $TEST_ENV/config/config0001.toml
632 2: $TEST_ENV/config/config0002.toml
633 Choose a config file (default 1): 1
634 [EOF]
635 ");
636
637 insta::assert_snapshot!(
638 std::fs::read_to_string(test_env.first_config_file_path()).unwrap(),
639 @r#"
640 test-key = "test-other-val"
641
642 [template-aliases]
643 'format_time_range(time_range)' = 'time_range.start() ++ " - " ++ time_range.end()'
644 "#);
645
646 insta::assert_snapshot!(
647 std::fs::read_to_string(test_env.last_config_file_path()).unwrap(),
648 @"");
649}
650
651#[test]
652fn test_config_set_for_repo() {
653 let test_env = TestEnvironment::default();
654 test_env.run_jj_in(".", ["git", "init", "repo"]).success();
655 let work_dir = test_env.work_dir("repo");
656 work_dir
657 .run_jj(["config", "set", "--repo", "test-key", "test-val"])
658 .success();
659 work_dir
660 .run_jj(["config", "set", "--repo", "test-table.foo", "true"])
661 .success();
662 // Ensure test-key successfully written to user config.
663 let repo_config_toml = work_dir.read_file(".jj/repo/config.toml");
664 insta::assert_snapshot!(repo_config_toml, @r#"
665 test-key = "test-val"
666
667 [test-table]
668 foo = true
669 "#);
670}
671
672#[test]
673fn test_config_set_toml_types() {
674 let mut test_env = TestEnvironment::default();
675 test_env.run_jj_in(".", ["git", "init", "repo"]).success();
676 // Test with fresh new config file
677 let user_config_path = test_env.config_path().join("config.toml");
678 test_env.set_config_path(&user_config_path);
679 let work_dir = test_env.work_dir("repo");
680
681 let set_value = |key, value| {
682 work_dir
683 .run_jj(["config", "set", "--user", key, value])
684 .success();
685 };
686 set_value("test-table.integer", "42");
687 set_value("test-table.float", "3.14");
688 set_value("test-table.array", r#"["one", "two"]"#);
689 set_value("test-table.boolean", "true");
690 set_value("test-table.string", r#""foo""#);
691 set_value("test-table.invalid", r"a + b");
692 insta::assert_snapshot!(std::fs::read_to_string(&user_config_path).unwrap(), @r#"
693 [test-table]
694 integer = 42
695 float = 3.14
696 array = ["one", "two"]
697 boolean = true
698 string = "foo"
699 invalid = "a + b"
700 "#);
701}
702
703#[test]
704fn test_config_set_type_mismatch() {
705 let test_env = TestEnvironment::default();
706 test_env.run_jj_in(".", ["git", "init", "repo"]).success();
707 let work_dir = test_env.work_dir("repo");
708
709 work_dir
710 .run_jj(["config", "set", "--user", "test-table.foo", "test-val"])
711 .success();
712 let output = work_dir.run_jj(["config", "set", "--user", "test-table", "not-a-table"]);
713 insta::assert_snapshot!(output, @r"
714 ------- stderr -------
715 Error: Failed to set test-table
716 Caused by: Would overwrite entire table test-table
717 [EOF]
718 [exit status: 1]
719 ");
720
721 // But it's fine to overwrite arrays and inline tables
722 work_dir
723 .run_jj(["config", "set", "--user", "test-table.array", "[1,2,3]"])
724 .success();
725 work_dir
726 .run_jj(["config", "set", "--user", "test-table.array", "[4,5,6]"])
727 .success();
728 work_dir
729 .run_jj(["config", "set", "--user", "test-table.inline", "{ x = 42}"])
730 .success();
731 work_dir
732 .run_jj(["config", "set", "--user", "test-table.inline", "42"])
733 .success();
734}
735
736#[test]
737fn test_config_set_nontable_parent() {
738 let test_env = TestEnvironment::default();
739 test_env.run_jj_in(".", ["git", "init", "repo"]).success();
740 let work_dir = test_env.work_dir("repo");
741
742 work_dir
743 .run_jj(["config", "set", "--user", "test-nontable", "test-val"])
744 .success();
745 let output = work_dir.run_jj(["config", "set", "--user", "test-nontable.foo", "test-val"]);
746 insta::assert_snapshot!(output, @r"
747 ------- stderr -------
748 Error: Failed to set test-nontable.foo
749 Caused by: Would overwrite non-table value with parent table test-nontable
750 [EOF]
751 [exit status: 1]
752 ");
753}
754
755#[test]
756fn test_config_unset_non_existent_key() {
757 let test_env = TestEnvironment::default();
758 test_env.run_jj_in(".", ["git", "init", "repo"]).success();
759 let work_dir = test_env.work_dir("repo");
760
761 let output = work_dir.run_jj(["config", "unset", "--user", "nonexistent"]);
762 insta::assert_snapshot!(output, @r#"
763 ------- stderr -------
764 Error: "nonexistent" doesn't exist
765 [EOF]
766 [exit status: 1]
767 "#);
768}
769
770#[test]
771fn test_config_unset_inline_table_key() {
772 let mut test_env = TestEnvironment::default();
773 // Test with fresh new config file
774 let user_config_path = test_env.config_path().join("config.toml");
775 test_env.set_config_path(&user_config_path);
776 test_env.run_jj_in(".", ["git", "init", "repo"]).success();
777 let work_dir = test_env.work_dir("repo");
778
779 work_dir
780 .run_jj(["config", "set", "--user", "inline-table", "{ foo = true }"])
781 .success();
782 work_dir
783 .run_jj(["config", "unset", "--user", "inline-table.foo"])
784 .success();
785 let user_config_toml = std::fs::read_to_string(&user_config_path).unwrap();
786 insta::assert_snapshot!(user_config_toml, @"inline-table = {}");
787}
788
789#[test]
790fn test_config_unset_table_like() {
791 let mut test_env = TestEnvironment::default();
792 // Test with fresh new config file
793 let user_config_path = test_env.config_path().join("config.toml");
794 test_env.set_config_path(&user_config_path);
795
796 std::fs::write(
797 &user_config_path,
798 indoc! {b"
799 inline-table = { foo = true }
800 [non-inline-table]
801 foo = true
802 "},
803 )
804 .unwrap();
805
806 // Inline table is syntactically a "value", so it can be deleted.
807 test_env
808 .run_jj_in(".", ["config", "unset", "--user", "inline-table"])
809 .success();
810 // Non-inline table cannot be deleted.
811 let output = test_env.run_jj_in(".", ["config", "unset", "--user", "non-inline-table"]);
812 insta::assert_snapshot!(output, @r"
813 ------- stderr -------
814 Error: Failed to unset non-inline-table
815 Caused by: Would delete entire table non-inline-table
816 [EOF]
817 [exit status: 1]
818 ");
819
820 let user_config_toml = std::fs::read_to_string(&user_config_path).unwrap();
821 insta::assert_snapshot!(user_config_toml, @r"
822 [non-inline-table]
823 foo = true
824 ");
825}
826
827#[test]
828fn test_config_unset_for_user() {
829 let mut test_env = TestEnvironment::default();
830 test_env.run_jj_in(".", ["git", "init", "repo"]).success();
831 // Test with fresh new config file
832 let user_config_path = test_env.config_path().join("config.toml");
833 test_env.set_config_path(&user_config_path);
834 let work_dir = test_env.work_dir("repo");
835
836 work_dir
837 .run_jj(["config", "set", "--user", "foo", "true"])
838 .success();
839 work_dir
840 .run_jj(["config", "unset", "--user", "foo"])
841 .success();
842
843 work_dir
844 .run_jj(["config", "set", "--user", "table.foo", "true"])
845 .success();
846 work_dir
847 .run_jj(["config", "unset", "--user", "table.foo"])
848 .success();
849
850 work_dir
851 .run_jj(["config", "set", "--user", "table.inline", "{ foo = true }"])
852 .success();
853 work_dir
854 .run_jj(["config", "unset", "--user", "table.inline"])
855 .success();
856
857 let user_config_toml = std::fs::read_to_string(&user_config_path).unwrap();
858 insta::assert_snapshot!(user_config_toml, @"[table]");
859}
860
861#[test]
862fn test_config_unset_for_repo() {
863 let test_env = TestEnvironment::default();
864 test_env.run_jj_in(".", ["git", "init", "repo"]).success();
865 let work_dir = test_env.work_dir("repo");
866
867 work_dir
868 .run_jj(["config", "set", "--repo", "test-key", "test-val"])
869 .success();
870 work_dir
871 .run_jj(["config", "unset", "--repo", "test-key"])
872 .success();
873
874 let repo_config_toml = work_dir.read_file(".jj/repo/config.toml");
875 insta::assert_snapshot!(repo_config_toml, @"");
876}
877
878#[test]
879fn test_config_edit_missing_opt() {
880 let test_env = TestEnvironment::default();
881 let output = test_env.run_jj_in(".", ["config", "edit"]);
882 insta::assert_snapshot!(output, @r"
883 ------- stderr -------
884 error: the following required arguments were not provided:
885 <--user|--repo>
886
887 Usage: jj config edit <--user|--repo>
888
889 For more information, try '--help'.
890 [EOF]
891 [exit status: 2]
892 ");
893}
894
895#[test]
896fn test_config_edit_user() {
897 let mut test_env = TestEnvironment::default();
898 test_env.run_jj_in(".", ["git", "init", "repo"]).success();
899 // Remove one of the config file to disambiguate
900 std::fs::remove_file(test_env.last_config_file_path()).unwrap();
901 let edit_script = test_env.set_up_fake_editor();
902 let work_dir = test_env.work_dir("repo");
903
904 std::fs::write(edit_script, "dump-path path").unwrap();
905 work_dir.run_jj(["config", "edit", "--user"]).success();
906
907 let edited_path =
908 PathBuf::from(std::fs::read_to_string(test_env.env_root().join("path")).unwrap());
909 assert_eq!(
910 edited_path,
911 dunce::simplified(&test_env.last_config_file_path())
912 );
913}
914
915#[test]
916fn test_config_edit_user_new_file() {
917 let mut test_env = TestEnvironment::default();
918 let user_config_path = test_env.config_path().join("config").join("file.toml");
919 test_env.set_up_fake_editor(); // set $EDIT_SCRIPT, but added configuration is ignored
920 test_env.add_env_var("EDITOR", fake_editor_path());
921 test_env.set_config_path(&user_config_path);
922 assert!(!user_config_path.exists());
923
924 test_env
925 .run_jj_in(".", ["config", "edit", "--user"])
926 .success();
927 assert!(
928 user_config_path.exists(),
929 "new file and directory should be created"
930 );
931}
932
933#[test]
934fn test_config_edit_repo() {
935 let mut test_env = TestEnvironment::default();
936 let edit_script = test_env.set_up_fake_editor();
937 test_env.run_jj_in(".", ["git", "init", "repo"]).success();
938 let work_dir = test_env.work_dir("repo");
939 let repo_config_path = work_dir
940 .root()
941 .join(PathBuf::from_iter([".jj", "repo", "config.toml"]));
942 assert!(!repo_config_path.exists());
943
944 std::fs::write(edit_script, "dump-path path").unwrap();
945 work_dir.run_jj(["config", "edit", "--repo"]).success();
946
947 let edited_path =
948 PathBuf::from(std::fs::read_to_string(test_env.env_root().join("path")).unwrap());
949 assert_eq!(edited_path, dunce::simplified(&repo_config_path));
950 assert!(repo_config_path.exists(), "new file should be created");
951}
952
953#[test]
954fn test_config_path() {
955 let mut test_env = TestEnvironment::default();
956 test_env.run_jj_in(".", ["git", "init", "repo"]).success();
957 let work_dir = test_env.work_dir("repo");
958
959 let user_config_path = test_env.env_root().join("config.toml");
960 let repo_config_path = work_dir
961 .root()
962 .join(PathBuf::from_iter([".jj", "repo", "config.toml"]));
963 test_env.set_config_path(&user_config_path);
964 let work_dir = test_env.work_dir("repo");
965
966 insta::assert_snapshot!(work_dir.run_jj(["config", "path", "--user"]), @r"
967 $TEST_ENV/config.toml
968 [EOF]
969 ");
970 assert!(
971 !user_config_path.exists(),
972 "jj config path shouldn't create new file"
973 );
974
975 insta::assert_snapshot!(work_dir.run_jj(["config", "path", "--repo"]), @r"
976 $TEST_ENV/repo/.jj/repo/config.toml
977 [EOF]
978 ");
979 assert!(
980 !repo_config_path.exists(),
981 "jj config path shouldn't create new file"
982 );
983
984 insta::assert_snapshot!(test_env.run_jj_in(".", ["config", "path", "--repo"]), @r"
985 ------- stderr -------
986 Error: No repo config path found
987 [EOF]
988 [exit status: 1]
989 ");
990}
991
992#[test]
993fn test_config_path_multiple() {
994 let mut test_env = TestEnvironment::default();
995 test_env.run_jj_in(".", ["git", "init", "repo"]).success();
996 let config_path = test_env.config_path().join("config.toml");
997 let work_config_path = test_env.config_path().join("conf.d");
998 let user_config_path = join_paths([config_path, work_config_path]).unwrap();
999 test_env.set_config_path(&user_config_path);
1000 let work_dir = test_env.work_dir("repo");
1001 insta::assert_snapshot!(work_dir.run_jj(["config", "path", "--user"]), @r"
1002 $TEST_ENV/config/config.toml
1003 $TEST_ENV/config/conf.d
1004 [EOF]
1005 ");
1006}
1007
1008#[test]
1009fn test_config_only_loads_toml_files() {
1010 let mut test_env = TestEnvironment::default();
1011 test_env.set_up_fake_editor();
1012 std::fs::File::create(test_env.config_path().join("is-not.loaded")).unwrap();
1013 insta::assert_snapshot!(test_env.run_jj_in(".", ["config", "edit", "--user"]), @r"
1014 ------- stderr -------
1015 1: $TEST_ENV/config/config0001.toml
1016 2: $TEST_ENV/config/config0002.toml
1017 Choose a config file (default 1): 1
1018 [EOF]
1019 ");
1020}
1021
1022#[test]
1023fn test_config_edit_repo_outside_repo() {
1024 let test_env = TestEnvironment::default();
1025 let output = test_env.run_jj_in(".", ["config", "edit", "--repo"]);
1026 insta::assert_snapshot!(output, @r"
1027 ------- stderr -------
1028 Error: No repo config path found to edit
1029 [EOF]
1030 [exit status: 1]
1031 ");
1032}
1033
1034#[test]
1035fn test_config_get() {
1036 let test_env = TestEnvironment::default();
1037 test_env.add_config(
1038 r#"
1039 [table]
1040 string = "some value 1"
1041 int = 123
1042 list = ["list", "value"]
1043 overridden = "foo"
1044 "#,
1045 );
1046 test_env.add_config(
1047 r#"
1048 [table]
1049 overridden = "bar"
1050 "#,
1051 );
1052
1053 let output = test_env.run_jj_in(".", ["config", "get", "nonexistent"]);
1054 insta::assert_snapshot!(output, @r"
1055 ------- stderr -------
1056 Config error: Value not found for nonexistent
1057 For help, see https://jj-vcs.github.io/jj/latest/config/ or use `jj help -k config`.
1058 [EOF]
1059 [exit status: 1]
1060 ");
1061
1062 let output = test_env.run_jj_in(".", ["config", "get", "table.string"]);
1063 insta::assert_snapshot!(output, @r"
1064 some value 1
1065 [EOF]
1066 ");
1067
1068 let output = test_env.run_jj_in(".", ["config", "get", "table.int"]);
1069 insta::assert_snapshot!(output, @r"
1070 123
1071 [EOF]
1072 ");
1073
1074 let output = test_env.run_jj_in(".", ["config", "get", "table.list"]);
1075 insta::assert_snapshot!(output, @r"
1076 ------- stderr -------
1077 Config error: Invalid type or value for table.list
1078 Caused by: Expected a value convertible to a string, but is an array
1079 Hint: Check the config file: $TEST_ENV/config/config0002.toml
1080 For help, see https://jj-vcs.github.io/jj/latest/config/ or use `jj help -k config`.
1081 [EOF]
1082 [exit status: 1]
1083 ");
1084
1085 let output = test_env.run_jj_in(".", ["config", "get", "table"]);
1086 insta::assert_snapshot!(output, @r"
1087 ------- stderr -------
1088 Config error: Invalid type or value for table
1089 Caused by: Expected a value convertible to a string, but is a table
1090 Hint: Check the config file: $TEST_ENV/config/config0003.toml
1091 For help, see https://jj-vcs.github.io/jj/latest/config/ or use `jj help -k config`.
1092 [EOF]
1093 [exit status: 1]
1094 ");
1095
1096 let output = test_env.run_jj_in(".", ["config", "get", "table.overridden"]);
1097 insta::assert_snapshot!(output, @r"
1098 bar
1099 [EOF]
1100 ");
1101}
1102
1103#[test]
1104fn test_config_path_syntax() {
1105 let test_env = TestEnvironment::default();
1106 test_env.add_config(
1107 r#"
1108 a.'b()' = 0
1109 'b c'.d = 1
1110 'b c'.e.'f[]' = 2
1111 - = 3
1112 _ = 4
1113 '.' = 5
1114 "#,
1115 );
1116
1117 let output = test_env.run_jj_in(".", ["config", "list", "a.'b()'"]);
1118 insta::assert_snapshot!(output, @r"
1119 a.'b()' = 0
1120 [EOF]
1121 ");
1122 let output = test_env.run_jj_in(".", ["config", "list", "'b c'"]);
1123 insta::assert_snapshot!(output, @r#"
1124 'b c'.d = 1
1125 'b c'.e."f[]" = 2
1126 [EOF]
1127 "#);
1128 let output = test_env.run_jj_in(".", ["config", "list", "'b c'.d"]);
1129 insta::assert_snapshot!(output, @r"
1130 'b c'.d = 1
1131 [EOF]
1132 ");
1133 let output = test_env.run_jj_in(".", ["config", "list", "'b c'.e.'f[]'"]);
1134 insta::assert_snapshot!(output, @r"
1135 'b c'.e.'f[]' = 2
1136 [EOF]
1137 ");
1138 let output = test_env.run_jj_in(".", ["config", "get", "'b c'.e.'f[]'"]);
1139 insta::assert_snapshot!(output, @r"
1140 2
1141 [EOF]
1142 ");
1143
1144 // Not a table
1145 let output = test_env.run_jj_in(".", ["config", "list", "a.'b()'.x"]);
1146 insta::assert_snapshot!(output, @r"
1147 ------- stderr -------
1148 Warning: No matching config key for a.'b()'.x
1149 [EOF]
1150 ");
1151 let output = test_env.run_jj_in(".", ["config", "get", "a.'b()'.x"]);
1152 insta::assert_snapshot!(output, @r"
1153 ------- stderr -------
1154 Config error: Value not found for a.'b()'.x
1155 For help, see https://jj-vcs.github.io/jj/latest/config/ or use `jj help -k config`.
1156 [EOF]
1157 [exit status: 1]
1158 ");
1159
1160 // "-" and "_" are valid TOML keys
1161 let output = test_env.run_jj_in(".", ["config", "list", "-"]);
1162 insta::assert_snapshot!(output, @r"
1163 - = 3
1164 [EOF]
1165 ");
1166 let output = test_env.run_jj_in(".", ["config", "list", "_"]);
1167 insta::assert_snapshot!(output, @r"
1168 _ = 4
1169 [EOF]
1170 ");
1171
1172 // "." requires quoting
1173 let output = test_env.run_jj_in(".", ["config", "list", "'.'"]);
1174 insta::assert_snapshot!(output, @r"
1175 '.' = 5
1176 [EOF]
1177 ");
1178 let output = test_env.run_jj_in(".", ["config", "get", "'.'"]);
1179 insta::assert_snapshot!(output, @r"
1180 5
1181 [EOF]
1182 ");
1183 let output = test_env.run_jj_in(".", ["config", "get", "."]);
1184 insta::assert_snapshot!(output, @r"
1185 ------- stderr -------
1186 error: invalid value '.' for '<NAME>': TOML parse error at line 1, column 1
1187 |
1188 1 | .
1189 | ^
1190 invalid key
1191
1192
1193 For more information, try '--help'.
1194 [EOF]
1195 [exit status: 2]
1196 ");
1197
1198 // Invalid TOML keys
1199 let output = test_env.run_jj_in(".", ["config", "list", "b c"]);
1200 insta::assert_snapshot!(output, @r"
1201 ------- stderr -------
1202 error: invalid value 'b c' for '[NAME]': TOML parse error at line 1, column 3
1203 |
1204 1 | b c
1205 | ^
1206
1207
1208
1209 For more information, try '--help'.
1210 [EOF]
1211 [exit status: 2]
1212 ");
1213 let output = test_env.run_jj_in(".", ["config", "list", ""]);
1214 insta::assert_snapshot!(output, @r"
1215 ------- stderr -------
1216 error: invalid value '' for '[NAME]': TOML parse error at line 1, column 1
1217 |
1218 1 |
1219 | ^
1220 invalid key
1221
1222
1223 For more information, try '--help'.
1224 [EOF]
1225 [exit status: 2]
1226 ");
1227}
1228
1229#[test]
1230#[cfg_attr(windows, ignore = "dirs::home_dir() can't be overridden by $HOME")] // TODO
1231fn test_config_conditional() {
1232 let mut test_env = TestEnvironment::default();
1233 let home_dir = test_env.work_dir(test_env.home_dir());
1234 home_dir.run_jj(["git", "init", "repo1"]).success();
1235 home_dir.run_jj(["git", "init", "repo2"]).success();
1236 // Test with fresh new config file
1237 let user_config_path = test_env.env_root().join("config.toml");
1238 test_env.set_config_path(&user_config_path);
1239 std::fs::write(
1240 &user_config_path,
1241 indoc! {"
1242 foo = 'global'
1243 baz = 'global'
1244 qux = 'global'
1245
1246 [[--scope]]
1247 --when.repositories = ['~/repo1']
1248 foo = 'repo1'
1249 [[--scope]]
1250 --when.repositories = ['~/repo2']
1251 foo = 'repo2'
1252
1253 [[--scope]]
1254 --when.commands = ['config']
1255 baz = 'config'
1256 [[--scope]]
1257 --when.commands = ['config get']
1258 qux = 'get'
1259 [[--scope]]
1260 --when.commands = ['config list']
1261 qux = 'list'
1262 "},
1263 )
1264 .unwrap();
1265 let home_dir = test_env.work_dir(test_env.home_dir());
1266 let work_dir1 = home_dir.dir("repo1");
1267 let work_dir2 = home_dir.dir("repo2");
1268
1269 // get and list should refer to the resolved config
1270 let output = test_env.run_jj_in(".", ["config", "get", "foo"]);
1271 insta::assert_snapshot!(output, @r"
1272 global
1273 [EOF]
1274 ");
1275 let output = work_dir1.run_jj(["config", "get", "foo"]);
1276 insta::assert_snapshot!(output, @r"
1277 repo1
1278 [EOF]
1279 ");
1280 // baz should be the same for `jj config get` and `jj config list`
1281 // qux should be different
1282 let output = work_dir1.run_jj(["config", "get", "baz"]);
1283 insta::assert_snapshot!(output, @r"
1284 config
1285 [EOF]
1286 ");
1287 let output = work_dir1.run_jj(["config", "get", "qux"]);
1288 insta::assert_snapshot!(output, @r"
1289 get
1290 [EOF]
1291 ");
1292 let output = test_env.run_jj_in(".", ["config", "list", "--user"]);
1293 insta::assert_snapshot!(output, @r"
1294 foo = 'global'
1295 baz = 'config'
1296 qux = 'list'
1297 [EOF]
1298 ");
1299 let output = work_dir1.run_jj(["config", "list", "--user"]);
1300 insta::assert_snapshot!(output, @r"
1301 foo = 'repo1'
1302 baz = 'config'
1303 qux = 'list'
1304 [EOF]
1305 ");
1306 let output = work_dir2.run_jj(["config", "list", "--user"]);
1307 insta::assert_snapshot!(output, @r"
1308 foo = 'repo2'
1309 baz = 'config'
1310 qux = 'list'
1311 [EOF]
1312 ");
1313
1314 // relative workspace path
1315 let output = work_dir2.run_jj(["config", "list", "--user", "-R../repo1"]);
1316 insta::assert_snapshot!(output, @r"
1317 foo = 'repo1'
1318 baz = 'config'
1319 qux = 'list'
1320 [EOF]
1321 ");
1322
1323 // set and unset should refer to the source config
1324 // (there's no option to update scoped table right now.)
1325 let output = test_env.run_jj_in(".", ["config", "set", "--user", "bar", "new value"]);
1326 insta::assert_snapshot!(output, @"");
1327 insta::assert_snapshot!(std::fs::read_to_string(&user_config_path).unwrap(), @r#"
1328 foo = 'global'
1329 baz = 'global'
1330 qux = 'global'
1331 bar = "new value"
1332
1333 [[--scope]]
1334 --when.repositories = ['~/repo1']
1335 foo = 'repo1'
1336 [[--scope]]
1337 --when.repositories = ['~/repo2']
1338 foo = 'repo2'
1339
1340 [[--scope]]
1341 --when.commands = ['config']
1342 baz = 'config'
1343 [[--scope]]
1344 --when.commands = ['config get']
1345 qux = 'get'
1346 [[--scope]]
1347 --when.commands = ['config list']
1348 qux = 'list'
1349 "#);
1350 let output = work_dir1.run_jj(["config", "unset", "--user", "foo"]);
1351 insta::assert_snapshot!(output, @"");
1352 insta::assert_snapshot!(std::fs::read_to_string(&user_config_path).unwrap(), @r#"
1353 baz = 'global'
1354 qux = 'global'
1355 bar = "new value"
1356
1357 [[--scope]]
1358 --when.repositories = ['~/repo1']
1359 foo = 'repo1'
1360 [[--scope]]
1361 --when.repositories = ['~/repo2']
1362 foo = 'repo2'
1363
1364 [[--scope]]
1365 --when.commands = ['config']
1366 baz = 'config'
1367 [[--scope]]
1368 --when.commands = ['config get']
1369 qux = 'get'
1370 [[--scope]]
1371 --when.commands = ['config list']
1372 qux = 'list'
1373 "#);
1374}
1375
1376// Minimal test for Windows where the home directory can't be switched.
1377// (Can be removed if test_config_conditional() is enabled on Windows.)
1378#[test]
1379fn test_config_conditional_without_home_dir() {
1380 let mut test_env = TestEnvironment::default();
1381 test_env.run_jj_in(".", ["git", "init", "repo"]).success();
1382 // Test with fresh new config file
1383 let user_config_path = test_env.env_root().join("config.toml");
1384 test_env.set_config_path(&user_config_path);
1385 let work_dir = test_env.work_dir("repo");
1386 std::fs::write(
1387 &user_config_path,
1388 format!(
1389 indoc! {"
1390 foo = 'global'
1391 [[--scope]]
1392 --when.repositories = [{repo_path}]
1393 foo = 'repo'
1394 "},
1395 // "\\?\" paths shouldn't be required on Windows
1396 repo_path = to_toml_value(dunce::simplified(work_dir.root()).to_str().unwrap())
1397 ),
1398 )
1399 .unwrap();
1400
1401 let output = test_env.run_jj_in(".", ["config", "get", "foo"]);
1402 insta::assert_snapshot!(output, @r"
1403 global
1404 [EOF]
1405 ");
1406 let output = work_dir.run_jj(["config", "get", "foo"]);
1407 insta::assert_snapshot!(output, @r"
1408 repo
1409 [EOF]
1410 ");
1411}
1412
1413#[test]
1414fn test_config_show_paths() {
1415 let test_env = TestEnvironment::default();
1416 test_env.run_jj_in(".", ["git", "init", "repo"]).success();
1417 let work_dir = test_env.work_dir("repo");
1418
1419 work_dir
1420 .run_jj(["config", "set", "--user", "ui.paginate", ":builtin"])
1421 .success();
1422 let output = test_env.run_jj_in(".", ["st"]);
1423 insta::assert_snapshot!(output, @r"
1424 ------- stderr -------
1425 Config error: Invalid type or value for ui.paginate
1426 Caused by: unknown variant `:builtin`, expected `never` or `auto`
1427
1428 Hint: Check the config file: $TEST_ENV/config/config0001.toml
1429 For help, see https://jj-vcs.github.io/jj/latest/config/ or use `jj help -k config`.
1430 [EOF]
1431 [exit status: 1]
1432 ");
1433}
1434
1435#[test]
1436fn test_config_author_change_warning() {
1437 let test_env = TestEnvironment::default();
1438 test_env.run_jj_in(".", ["git", "init", "repo"]).success();
1439 let work_dir = test_env.work_dir("repo");
1440 let output = work_dir.run_jj(["config", "set", "--repo", "user.email", "'Foo'"]);
1441 insta::assert_snapshot!(output, @r#"
1442 ------- stderr -------
1443 Warning: This setting will only impact future commits.
1444 The author of the working copy will stay "Test User <test.user@example.com>".
1445 To change the working copy author, use "jj describe --reset-author --no-edit"
1446 [EOF]
1447 "#);
1448
1449 // test_env.run_jj*() resets state for every invocation
1450 // for this test, the state (user.email) is needed
1451 work_dir
1452 .run_jj_with(|cmd| {
1453 cmd.args(["describe", "--reset-author", "--no-edit"])
1454 .env_remove("JJ_EMAIL")
1455 })
1456 .success();
1457
1458 let output = work_dir.run_jj(["log"]);
1459 insta::assert_snapshot!(output, @r"
1460 @ qpvuntsm Foo 2001-02-03 08:05:09 ed1febd8
1461 │ (empty) (no description set)
1462 ◆ zzzzzzzz root() 00000000
1463 [EOF]
1464 ");
1465}
1466
1467#[test]
1468fn test_config_author_change_warning_root_env() {
1469 let test_env = TestEnvironment::default();
1470 let output = test_env.run_jj_in(".", ["config", "set", "--user", "user.email", "'Foo'"]);
1471 insta::assert_snapshot!(output, @"");
1472}
1473
1474fn find_stdout_lines(keyname_pattern: &str, stdout: &str) -> String {
1475 let key_line_re = Regex::new(&format!(r"(?m)^{keyname_pattern} = .*\n")).unwrap();
1476 key_line_re.find_iter(stdout).map(|m| m.as_str()).collect()
1477}