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 crate::common::TestEnvironment;
16
17#[test]
18fn test_alias_basic() {
19 let test_env = TestEnvironment::default();
20 test_env.run_jj_in(".", ["git", "init", "repo"]).success();
21 let work_dir = test_env.work_dir("repo");
22
23 test_env.add_config(r#"aliases.bk = ["log", "-r", "@", "-T", "bookmarks"]"#);
24 work_dir
25 .run_jj(["bookmark", "create", "my-bookmark", "-r", "@"])
26 .success();
27 let output = work_dir.run_jj(["bk"]);
28 insta::assert_snapshot!(output, @r"
29 @ my-bookmark
30 │
31 ~
32 [EOF]
33 ");
34}
35
36#[test]
37fn test_alias_bad_name() {
38 let test_env = TestEnvironment::default();
39 test_env.run_jj_in(".", ["git", "init", "repo"]).success();
40 let work_dir = test_env.work_dir("repo");
41
42 let output = work_dir.run_jj(["foo."]);
43 insta::assert_snapshot!(output, @r"
44 ------- stderr -------
45 error: unrecognized subcommand 'foo.'
46
47 Usage: jj [OPTIONS] <COMMAND>
48
49 For more information, try '--help'.
50 [EOF]
51 [exit status: 2]
52 ");
53}
54
55#[test]
56fn test_alias_calls_empty_command() {
57 let test_env = TestEnvironment::default();
58 test_env.run_jj_in(".", ["git", "init", "repo"]).success();
59 let work_dir = test_env.work_dir("repo");
60
61 test_env.add_config(
62 r#"
63 aliases.empty = []
64 aliases.empty_command_with_opts = ["--no-pager"]
65 "#,
66 );
67
68 let output = work_dir.run_jj(["empty"]);
69 insta::assert_snapshot!(
70 output.normalize_stderr_with(|s| s.split_inclusive('\n').take(3).collect()), @r"
71 ------- stderr -------
72 Jujutsu (An experimental VCS)
73
74 Usage: jj [OPTIONS] <COMMAND>
75 [EOF]
76 [exit status: 2]
77 ");
78 let output = work_dir.run_jj(["empty", "--no-pager"]);
79 insta::assert_snapshot!(
80 output.normalize_stderr_with(|s| s.split_inclusive('\n').take(1).collect()), @r"
81 ------- stderr -------
82 error: 'jj' requires a subcommand but one was not provided
83 [EOF]
84 [exit status: 2]
85 ");
86 let output = work_dir.run_jj(["empty_command_with_opts"]);
87 insta::assert_snapshot!(
88 output.normalize_stderr_with(|s| s.split_inclusive('\n').take(1).collect()), @r"
89 ------- stderr -------
90 error: 'jj' requires a subcommand but one was not provided
91 [EOF]
92 [exit status: 2]
93 ");
94}
95
96#[test]
97fn test_alias_calls_unknown_command() {
98 let test_env = TestEnvironment::default();
99 test_env.run_jj_in(".", ["git", "init", "repo"]).success();
100 let work_dir = test_env.work_dir("repo");
101
102 test_env.add_config(r#"aliases.foo = ["nonexistent"]"#);
103 let output = work_dir.run_jj(["foo"]);
104 insta::assert_snapshot!(output, @r"
105 ------- stderr -------
106 error: unrecognized subcommand 'nonexistent'
107
108 tip: a similar subcommand exists: 'next'
109
110 Usage: jj [OPTIONS] <COMMAND>
111
112 For more information, try '--help'.
113 [EOF]
114 [exit status: 2]
115 ");
116}
117
118#[test]
119fn test_alias_calls_command_with_invalid_option() {
120 let test_env = TestEnvironment::default();
121 test_env.run_jj_in(".", ["git", "init", "repo"]).success();
122 let work_dir = test_env.work_dir("repo");
123
124 test_env.add_config(r#"aliases.foo = ["log", "--nonexistent"]"#);
125 let output = work_dir.run_jj(["foo"]);
126 insta::assert_snapshot!(output, @r"
127 ------- stderr -------
128 error: unexpected argument '--nonexistent' found
129
130 tip: to pass '--nonexistent' as a value, use '-- --nonexistent'
131
132 Usage: jj log [OPTIONS] [FILESETS]...
133
134 For more information, try '--help'.
135 [EOF]
136 [exit status: 2]
137 ");
138}
139
140#[test]
141fn test_alias_calls_help() {
142 let test_env = TestEnvironment::default();
143 test_env.run_jj_in(".", ["git", "init", "repo"]).success();
144 let work_dir = test_env.work_dir("repo");
145 test_env.add_config(r#"aliases.h = ["--help"]"#);
146 let output = work_dir.run_jj(&["h"]);
147 insta::assert_snapshot!(
148 output.normalize_stdout_with(|s| s.split_inclusive('\n').take(7).collect()), @r"
149 Jujutsu (An experimental VCS)
150
151 To get started, see the tutorial [`jj help -k tutorial`].
152
153 [`jj help -k tutorial`]: https://jj-vcs.github.io/jj/latest/tutorial/
154
155 Usage: jj [OPTIONS] <COMMAND>
156 [EOF]
157 ");
158}
159
160#[test]
161fn test_alias_cannot_override_builtin() {
162 let test_env = TestEnvironment::default();
163 test_env.run_jj_in(".", ["git", "init", "repo"]).success();
164 let work_dir = test_env.work_dir("repo");
165
166 test_env.add_config(r#"aliases.log = ["rebase"]"#);
167 // Alias should give a warning
168 let output = work_dir.run_jj(["log", "-r", "root()"]);
169 insta::assert_snapshot!(output, @r"
170 ◆ zzzzzzzz root() 00000000
171 [EOF]
172 ------- stderr -------
173 Warning: Cannot define an alias that overrides the built-in command 'log'
174 [EOF]
175 ");
176}
177
178#[test]
179fn test_alias_recursive() {
180 let test_env = TestEnvironment::default();
181 test_env.run_jj_in(".", ["git", "init", "repo"]).success();
182 let work_dir = test_env.work_dir("repo");
183
184 test_env.add_config(
185 r#"[aliases]
186 foo = ["foo"]
187 bar = ["baz"]
188 baz = ["bar"]
189 "#,
190 );
191 // Alias should not cause infinite recursion or hang
192 let output = work_dir.run_jj(["foo"]);
193 insta::assert_snapshot!(output, @r"
194 ------- stderr -------
195 Error: Recursive alias definition involving `foo`
196 [EOF]
197 [exit status: 1]
198 ");
199 // Also test with mutual recursion
200 let output = work_dir.run_jj(["bar"]);
201 insta::assert_snapshot!(output, @r"
202 ------- stderr -------
203 Error: Recursive alias definition involving `bar`
204 [EOF]
205 [exit status: 1]
206 ");
207}
208
209#[test]
210fn test_alias_global_args_before_and_after() {
211 let test_env = TestEnvironment::default();
212 test_env.run_jj_in(".", ["git", "init", "repo"]).success();
213 let work_dir = test_env.work_dir("repo");
214 test_env.add_config(r#"aliases.l = ["log", "-T", "commit_id", "-r", "all()"]"#);
215 // Test the setup
216 let output = work_dir.run_jj(["l"]);
217 insta::assert_snapshot!(output, @r"
218 @ 230dd059e1b059aefc0da06a2e5a7dbf22362f22
219 ◆ 0000000000000000000000000000000000000000
220 [EOF]
221 ");
222
223 // Can pass global args before
224 let output = work_dir.run_jj(["l", "--at-op", "@-"]);
225 insta::assert_snapshot!(output, @r"
226 ◆ 0000000000000000000000000000000000000000
227 [EOF]
228 ");
229 // Can pass global args after
230 let output = work_dir.run_jj(["--at-op", "@-", "l"]);
231 insta::assert_snapshot!(output, @r"
232 ◆ 0000000000000000000000000000000000000000
233 [EOF]
234 ");
235 // Test passing global args both before and after
236 let output = work_dir.run_jj(["--at-op", "abc123", "l", "--at-op", "@-"]);
237 insta::assert_snapshot!(output, @r"
238 ◆ 0000000000000000000000000000000000000000
239 [EOF]
240 ");
241 let output = work_dir.run_jj(["-R", "../nonexistent", "l", "-R", "."]);
242 insta::assert_snapshot!(output, @r"
243 @ 230dd059e1b059aefc0da06a2e5a7dbf22362f22
244 ◆ 0000000000000000000000000000000000000000
245 [EOF]
246 ");
247}
248
249#[test]
250fn test_alias_global_args_in_definition() {
251 let test_env = TestEnvironment::default();
252 test_env.run_jj_in(".", ["git", "init", "repo"]).success();
253 let work_dir = test_env.work_dir("repo");
254 test_env.add_config(
255 r#"aliases.l = ["log", "-T", "commit_id", "--at-op", "@-", "-r", "all()", "--color=always"]"#,
256 );
257
258 // The global argument in the alias is respected
259 let output = work_dir.run_jj(["l"]);
260 insta::assert_snapshot!(output, @r"
261 [1m[38;5;14m◆[0m [38;5;4m0000000000000000000000000000000000000000[39m
262 [EOF]
263 ");
264}
265
266#[test]
267fn test_alias_invalid_definition() {
268 let test_env = TestEnvironment::default();
269
270 test_env.add_config(
271 r#"[aliases]
272 non-list = 5
273 non-string-list = [0]
274 "#,
275 );
276 let output = test_env.run_jj_in(".", ["non-list"]);
277 insta::assert_snapshot!(output.normalize_backslash(), @r"
278 ------- stderr -------
279 Config error: Invalid type or value for aliases.non-list
280 Caused by: invalid type: integer `5`, expected a sequence
281
282 Hint: Check the config file: $TEST_ENV/config/config0002.toml
283 For help, see https://jj-vcs.github.io/jj/latest/config/ or use `jj help -k config`.
284 [EOF]
285 [exit status: 1]
286 ");
287 let output = test_env.run_jj_in(".", ["non-string-list"]);
288 insta::assert_snapshot!(output, @r"
289 ------- stderr -------
290 Config error: Invalid type or value for aliases.non-string-list
291 Caused by: invalid type: integer `0`, expected a string
292
293 Hint: Check the config file: $TEST_ENV/config/config0002.toml
294 For help, see https://jj-vcs.github.io/jj/latest/config/ or use `jj help -k config`.
295 [EOF]
296 [exit status: 1]
297 ");
298}
299
300#[test]
301fn test_alias_in_repo_config() {
302 let test_env = TestEnvironment::default();
303 test_env.run_jj_in(".", ["git", "init", "repo1"]).success();
304 let work_dir1 = test_env.work_dir("repo1");
305 work_dir1.create_dir("sub");
306 test_env.run_jj_in(".", ["git", "init", "repo2"]).success();
307 let work_dir2 = test_env.work_dir("repo2");
308 work_dir2.create_dir("sub");
309
310 test_env.add_config(r#"aliases.l = ['log', '-r@', '--no-graph', '-T"user alias\n"']"#);
311 work_dir1.write_file(
312 ".jj/repo/config.toml",
313 r#"aliases.l = ['log', '-r@', '--no-graph', '-T"repo1 alias\n"']"#,
314 );
315
316 // In repo1 sub directory, aliases can be loaded from the repo1 config.
317 let output = test_env.run_jj_in(work_dir1.root().join("sub"), ["l"]);
318 insta::assert_snapshot!(output, @r"
319 repo1 alias
320 [EOF]
321 ");
322
323 // In repo2 directory, no repo-local aliases exist.
324 let output = work_dir2.run_jj(["l"]);
325 insta::assert_snapshot!(output, @r"
326 user alias
327 [EOF]
328 ");
329
330 // Aliases can't be loaded from the -R path due to chicken and egg problem.
331 let output = work_dir2.run_jj(["l", "-R", work_dir1.root().to_str().unwrap()]);
332 insta::assert_snapshot!(output, @r"
333 user alias
334 [EOF]
335 ------- stderr -------
336 Warning: Command aliases cannot be loaded from -R/--repository path or --config/--config-file arguments.
337 [EOF]
338 ");
339
340 // Aliases are loaded from the cwd-relative workspace even with -R.
341 let output = work_dir1.run_jj(["l", "-R", work_dir2.root().to_str().unwrap()]);
342 insta::assert_snapshot!(output, @r"
343 repo1 alias
344 [EOF]
345 ------- stderr -------
346 Warning: Command aliases cannot be loaded from -R/--repository path or --config/--config-file arguments.
347 [EOF]
348 ");
349
350 // No warning if the expanded command is identical.
351 let output = work_dir1.run_jj(["file", "list", "-R", work_dir2.root().to_str().unwrap()]);
352 insta::assert_snapshot!(output, @"");
353
354 // Config loaded from the cwd-relative workspace shouldn't persist. It's
355 // used only for command arguments expansion.
356 let output = work_dir1.run_jj([
357 "config",
358 "list",
359 "aliases",
360 "-R",
361 work_dir2.root().to_str().unwrap(),
362 ]);
363 insta::assert_snapshot!(output, @r#"
364 aliases.l = ['log', '-r@', '--no-graph', '-T"user alias\n"']
365 [EOF]
366 "#);
367}
368
369#[test]
370fn test_alias_in_config_arg() {
371 let test_env = TestEnvironment::default();
372 test_env.run_jj_in(".", ["git", "init", "repo"]).success();
373 let work_dir = test_env.work_dir("repo");
374 test_env.add_config(r#"aliases.l = ['log', '-r@', '--no-graph', '-T"user alias\n"']"#);
375
376 let output = work_dir.run_jj(["l"]);
377 insta::assert_snapshot!(output, @r"
378 user alias
379 [EOF]
380 ");
381
382 let alias_arg = r#"--config=aliases.l=['log', '-r@', '--no-graph', '-T"arg alias\n"']"#;
383 let output = work_dir.run_jj([alias_arg, "l"]);
384 insta::assert_snapshot!(output, @r"
385 user alias
386 [EOF]
387 ------- stderr -------
388 Warning: Command aliases cannot be loaded from -R/--repository path or --config/--config-file arguments.
389 [EOF]
390 ");
391 // should print warning about aliases even if cli parsing fails
392 let alias_arg = r#"--config=aliases.this-command-not-exist=['log', '-r@', '--no-graph', '-T"arg alias\n"']"#;
393 let output = work_dir.run_jj([alias_arg, "this-command-not-exist"]);
394 insta::assert_snapshot!(output, @r"
395 ------- stderr -------
396 Warning: Command aliases cannot be loaded from -R/--repository path or --config/--config-file arguments.
397 error: unrecognized subcommand 'this-command-not-exist'
398
399 Usage: jj [OPTIONS] <COMMAND>
400
401 For more information, try '--help'.
402 [EOF]
403 [exit status: 2]
404 ");
405}
406
407#[test]
408fn test_aliases_overriding_friendly_errors() {
409 let test_env = TestEnvironment::default();
410 // Test with color
411 let output = test_env.run_jj_in(".", ["--color=always", "init", "repo"]);
412 insta::assert_snapshot!(output, @r#"
413 ------- stderr -------
414 [1m[31merror:[0m unrecognized subcommand '[33minit[0m'
415
416 For more information, try '[1m[32m--help[0m'.
417 [1m[38;5;6mHint: [0m[39mYou probably want `jj git init`. See also `jj help git`.[39m
418 [1m[38;5;6mHint: [0m[39mYou can configure `aliases.init = ["git", "init"]` if you want `jj init` to work and always use the Git backend.[39m
419 [EOF]
420 [exit status: 2]
421 "#);
422
423 let output = test_env.run_jj_in(".", ["init", "repo"]);
424 insta::assert_snapshot!(output, @r#"
425 ------- stderr -------
426 error: unrecognized subcommand 'init'
427
428 For more information, try '--help'.
429 Hint: You probably want `jj git init`. See also `jj help git`.
430 Hint: You can configure `aliases.init = ["git", "init"]` if you want `jj init` to work and always use the Git backend.
431 [EOF]
432 [exit status: 2]
433 "#);
434 let output = test_env.run_jj_in(".", ["clone", "https://example.org/repo"]);
435 insta::assert_snapshot!(output, @r#"
436 ------- stderr -------
437 error: unrecognized subcommand 'clone'
438
439 For more information, try '--help'.
440 Hint: You probably want `jj git clone`. See also `jj help git`.
441 Hint: You can configure `aliases.clone = ["git", "clone"]` if you want `jj clone` to work and always use the Git backend.
442 [EOF]
443 [exit status: 2]
444 "#);
445 let output = test_env.run_jj_in(".", ["init", "--help"]);
446 insta::assert_snapshot!(output, @r#"
447 ------- stderr -------
448 error: unrecognized subcommand 'init'
449
450 For more information, try '--help'.
451 Hint: You probably want `jj git init`. See also `jj help git`.
452 Hint: You can configure `aliases.init = ["git", "init"]` if you want `jj init` to work and always use the Git backend.
453 [EOF]
454 [exit status: 2]
455 "#);
456
457 // Test that `init` can be overridden as an alias. (We use `jj config get`
458 // as a command with a predictable output)
459 test_env.add_config(r#"aliases.init=["config", "get", "user.name"]"#);
460 let output = test_env.run_jj_in(".", ["init"]);
461 insta::assert_snapshot!(output, @r"
462 Test User
463 [EOF]
464 ");
465}