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::path;
16
17use indoc::formatdoc;
18use test_case::test_case;
19use testutils::git;
20
21use crate::common::to_toml_value;
22use crate::common::CommandOutput;
23use crate::common::TestEnvironment;
24use crate::common::TestWorkDir;
25
26fn set_up_non_empty_git_repo(git_repo: &gix::Repository) {
27 set_up_git_repo_with_file(git_repo, "file");
28}
29
30fn set_up_git_repo_with_file(git_repo: &gix::Repository, filename: &str) {
31 git::add_commit(
32 git_repo,
33 "refs/heads/main",
34 filename,
35 b"content",
36 "message",
37 &[],
38 );
39 git::set_symbolic_reference(git_repo, "HEAD", "refs/heads/main");
40}
41
42#[cfg_attr(feature = "git2", test_case(false; "use git2 for remote calls"))]
43#[test_case(true; "spawn a git subprocess for remote calls")]
44fn test_git_clone(subprocess: bool) {
45 let test_env = TestEnvironment::default();
46 let root_dir = test_env.work_dir("");
47 test_env.add_config("git.auto-local-bookmark = true");
48 if !subprocess {
49 test_env.add_config("git.subprocess = false");
50 }
51 let git_repo_path = test_env.env_root().join("source");
52 let git_repo = git::init(git_repo_path);
53
54 // Clone an empty repo
55 let output = root_dir.run_jj(["git", "clone", "source", "empty"]);
56 insta::allow_duplicates! {
57 insta::assert_snapshot!(output, @r#"
58 ------- stderr -------
59 Fetching into new repo in "$TEST_ENV/empty"
60 Nothing changed.
61 [EOF]
62 "#);
63 }
64
65 set_up_non_empty_git_repo(&git_repo);
66
67 // Clone with relative source path
68 let output = root_dir.run_jj(["git", "clone", "source", "clone"]);
69 insta::allow_duplicates! {
70 insta::assert_snapshot!(output, @r#"
71 ------- stderr -------
72 Fetching into new repo in "$TEST_ENV/clone"
73 bookmark: main@origin [new] tracked
74 Setting the revset alias `trunk()` to `main@origin`
75 Working copy (@) now at: uuqppmxq f78d2645 (empty) (no description set)
76 Parent commit (@-) : qomsplrm ebeb70d8 main | message
77 Added 1 files, modified 0 files, removed 0 files
78 [EOF]
79 "#);
80 }
81 let clone_dir = test_env.work_dir("clone");
82 assert!(clone_dir.root().join("file").exists());
83
84 // Subsequent fetch should just work even if the source path was relative
85 let output = clone_dir.run_jj(["git", "fetch"]);
86 insta::allow_duplicates! {
87 insta::assert_snapshot!(output, @r"
88 ------- stderr -------
89 Nothing changed.
90 [EOF]
91 ");
92 }
93
94 // Failed clone should clean up the destination directory
95 root_dir.create_dir("bad");
96 let output = root_dir.run_jj(["git", "clone", "bad", "failed"]);
97 // git2's internal error is slightly different
98 if subprocess {
99 insta::assert_snapshot!(output, @r#"
100 ------- stderr -------
101 Fetching into new repo in "$TEST_ENV/failed"
102 Error: Could not find repository at '$TEST_ENV/bad'
103 [EOF]
104 [exit status: 1]
105 "#);
106 } else {
107 insta::assert_snapshot!(output, @r#"
108 ------- stderr -------
109 Fetching into new repo in "$TEST_ENV/failed"
110 Error: could not find repository at '$TEST_ENV/bad'; class=Repository (6)
111 [EOF]
112 [exit status: 1]
113 "#);
114 }
115 assert!(!test_env.env_root().join("failed").exists());
116
117 // Failed clone shouldn't remove the existing destination directory
118 let failed_dir = root_dir.create_dir("failed");
119 let output = root_dir.run_jj(["git", "clone", "bad", "failed"]);
120 // git2's internal error is slightly different
121 if subprocess {
122 insta::assert_snapshot!(output, @r#"
123 ------- stderr -------
124 Fetching into new repo in "$TEST_ENV/failed"
125 Error: Could not find repository at '$TEST_ENV/bad'
126 [EOF]
127 [exit status: 1]
128 "#);
129 } else {
130 insta::assert_snapshot!(output, @r#"
131 ------- stderr -------
132 Fetching into new repo in "$TEST_ENV/failed"
133 Error: could not find repository at '$TEST_ENV/bad'; class=Repository (6)
134 [EOF]
135 [exit status: 1]
136 "#);
137 }
138 assert!(failed_dir.root().exists());
139 assert!(!failed_dir.root().join(".jj").exists());
140
141 // Failed clone (if attempted) shouldn't remove the existing workspace
142 let output = root_dir.run_jj(["git", "clone", "bad", "clone"]);
143 insta::allow_duplicates! {
144 insta::assert_snapshot!(output, @r"
145 ------- stderr -------
146 Error: Destination path exists and is not an empty directory
147 [EOF]
148 [exit status: 1]
149 ");
150 }
151 assert!(clone_dir.root().join(".jj").exists());
152
153 // Try cloning into an existing workspace
154 let output = root_dir.run_jj(["git", "clone", "source", "clone"]);
155 insta::allow_duplicates! {
156 insta::assert_snapshot!(output, @r"
157 ------- stderr -------
158 Error: Destination path exists and is not an empty directory
159 [EOF]
160 [exit status: 1]
161 ");
162 }
163
164 // Try cloning into an existing file
165 root_dir.write_file("file", "contents");
166 let output = root_dir.run_jj(["git", "clone", "source", "file"]);
167 insta::allow_duplicates! {
168 insta::assert_snapshot!(output, @r"
169 ------- stderr -------
170 Error: Destination path exists and is not an empty directory
171 [EOF]
172 [exit status: 1]
173 ");
174 }
175
176 // Try cloning into non-empty, non-workspace directory
177 clone_dir.remove_dir_all(".jj");
178 let output = root_dir.run_jj(["git", "clone", "source", "clone"]);
179 insta::allow_duplicates! {
180 insta::assert_snapshot!(output, @r"
181 ------- stderr -------
182 Error: Destination path exists and is not an empty directory
183 [EOF]
184 [exit status: 1]
185 ");
186 }
187
188 // Clone into a nested path
189 let output = root_dir.run_jj(["git", "clone", "source", "nested/path/to/repo"]);
190 insta::allow_duplicates! {
191 insta::assert_snapshot!(output, @r#"
192 ------- stderr -------
193 Fetching into new repo in "$TEST_ENV/nested/path/to/repo"
194 bookmark: main@origin [new] tracked
195 Setting the revset alias `trunk()` to `main@origin`
196 Working copy (@) now at: uuzqqzqu cf5d593e (empty) (no description set)
197 Parent commit (@-) : qomsplrm ebeb70d8 main | message
198 Added 1 files, modified 0 files, removed 0 files
199 [EOF]
200 "#);
201 }
202}
203
204#[cfg_attr(feature = "git2", test_case(false; "use git2 for remote calls"))]
205#[test_case(true; "spawn a git subprocess for remote calls")]
206fn test_git_clone_bad_source(subprocess: bool) {
207 let test_env = TestEnvironment::default();
208 let root_dir = test_env.work_dir("");
209 if !subprocess {
210 test_env.add_config("git.subprocess = false");
211 }
212
213 let output = root_dir.run_jj(["git", "clone", "", "dest"]);
214 insta::allow_duplicates! {
215 insta::assert_snapshot!(output, @r#"
216 ------- stderr -------
217 Error: local path "" does not specify a path to a repository
218 [EOF]
219 [exit status: 2]
220 "#);
221 }
222
223 // Invalid port number
224 let output = root_dir.run_jj(["git", "clone", "https://example.net:bad-port/bar", "dest"]);
225 insta::allow_duplicates! {
226 insta::assert_snapshot!(output, @r#"
227 ------- stderr -------
228 Error: URL "https://example.net:bad-port/bar" can not be parsed as valid URL
229 Caused by: invalid port number
230 [EOF]
231 [exit status: 2]
232 "#);
233 }
234}
235
236#[cfg_attr(feature = "git2", test_case(false; "use git2 for remote calls"))]
237#[test_case(true; "spawn a git subprocess for remote calls")]
238fn test_git_clone_colocate(subprocess: bool) {
239 let test_env = TestEnvironment::default();
240 let root_dir = test_env.work_dir("");
241 test_env.add_config("git.auto-local-bookmark = true");
242 if !subprocess {
243 test_env.add_config("git.subprocess = false");
244 }
245 let git_repo_path = test_env.env_root().join("source");
246 let git_repo = git::init(git_repo_path);
247
248 // Clone an empty repo
249 let output = root_dir.run_jj(["git", "clone", "source", "empty", "--colocate"]);
250 insta::allow_duplicates! {
251 insta::assert_snapshot!(output, @r#"
252 ------- stderr -------
253 Fetching into new repo in "$TEST_ENV/empty"
254 Nothing changed.
255 [EOF]
256 "#);
257 }
258
259 // git_target path should be relative to the store
260 let empty_dir = test_env.work_dir("empty");
261 let git_target_file_contents =
262 String::from_utf8(empty_dir.read_file(".jj/repo/store/git_target").into()).unwrap();
263 insta::allow_duplicates! {
264 insta::assert_snapshot!(
265 git_target_file_contents.replace(path::MAIN_SEPARATOR, "/"),
266 @"../../../.git");
267 }
268
269 set_up_non_empty_git_repo(&git_repo);
270
271 // Clone with relative source path
272 let output = root_dir.run_jj(["git", "clone", "source", "clone", "--colocate"]);
273 insta::allow_duplicates! {
274 insta::assert_snapshot!(output, @r#"
275 ------- stderr -------
276 Fetching into new repo in "$TEST_ENV/clone"
277 bookmark: main@origin [new] tracked
278 Setting the revset alias `trunk()` to `main@origin`
279 Working copy (@) now at: uuqppmxq f78d2645 (empty) (no description set)
280 Parent commit (@-) : qomsplrm ebeb70d8 main | message
281 Added 1 files, modified 0 files, removed 0 files
282 [EOF]
283 "#);
284 }
285 let clone_dir = test_env.work_dir("clone");
286 assert!(clone_dir.root().join("file").exists());
287 assert!(clone_dir.root().join(".git").exists());
288
289 eprintln!(
290 "{:?}",
291 git_repo.head().expect("Repo head should be set").name()
292 );
293
294 let jj_git_repo = git::open(clone_dir.root());
295 assert_eq!(
296 jj_git_repo
297 .head_id()
298 .expect("Clone Repo HEAD should be set.")
299 .detach(),
300 git_repo
301 .head_id()
302 .expect("Repo HEAD should be set.")
303 .detach(),
304 );
305 // ".jj" directory should be ignored at Git side.
306 let git_statuses = git::status(&jj_git_repo);
307 insta::allow_duplicates! {
308 insta::assert_debug_snapshot!(git_statuses, @r#"
309 [
310 GitStatus {
311 path: ".jj/.gitignore",
312 status: Worktree(
313 Ignored,
314 ),
315 },
316 GitStatus {
317 path: ".jj/repo",
318 status: Worktree(
319 Ignored,
320 ),
321 },
322 GitStatus {
323 path: ".jj/working_copy",
324 status: Worktree(
325 Ignored,
326 ),
327 },
328 ]
329 "#);
330 }
331
332 // The old default bookmark "master" shouldn't exist.
333 insta::allow_duplicates! {
334 insta::assert_snapshot!(get_bookmark_output(&clone_dir), @r"
335 main: qomsplrm ebeb70d8 message
336 @git: qomsplrm ebeb70d8 message
337 @origin: qomsplrm ebeb70d8 message
338 [EOF]
339 ");
340 }
341
342 // Subsequent fetch should just work even if the source path was relative
343 let output = clone_dir.run_jj(["git", "fetch"]);
344 insta::allow_duplicates! {
345 insta::assert_snapshot!(output, @r"
346 ------- stderr -------
347 Nothing changed.
348 [EOF]
349 ");
350 }
351
352 // Failed clone should clean up the destination directory
353 root_dir.create_dir("bad");
354 let output = root_dir.run_jj(["git", "clone", "--colocate", "bad", "failed"]);
355 // git2's internal error is slightly different
356 if subprocess {
357 insta::assert_snapshot!(output, @r#"
358 ------- stderr -------
359 Fetching into new repo in "$TEST_ENV/failed"
360 Error: Could not find repository at '$TEST_ENV/bad'
361 [EOF]
362 [exit status: 1]
363 "#);
364 } else {
365 insta::assert_snapshot!(output, @r#"
366 ------- stderr -------
367 Fetching into new repo in "$TEST_ENV/failed"
368 Error: could not find repository at '$TEST_ENV/bad'; class=Repository (6)
369 [EOF]
370 [exit status: 1]
371 "#);
372 }
373 assert!(!test_env.env_root().join("failed").exists());
374
375 // Failed clone shouldn't remove the existing destination directory
376 let failed_dir = root_dir.create_dir("failed");
377 let output = root_dir.run_jj(["git", "clone", "--colocate", "bad", "failed"]);
378 // git2's internal error is slightly different
379 if subprocess {
380 insta::assert_snapshot!(output, @r#"
381 ------- stderr -------
382 Fetching into new repo in "$TEST_ENV/failed"
383 Error: Could not find repository at '$TEST_ENV/bad'
384 [EOF]
385 [exit status: 1]
386 "#);
387 } else {
388 insta::assert_snapshot!(output, @r#"
389 ------- stderr -------
390 Fetching into new repo in "$TEST_ENV/failed"
391 Error: could not find repository at '$TEST_ENV/bad'; class=Repository (6)
392 [EOF]
393 [exit status: 1]
394 "#);
395 }
396 assert!(failed_dir.root().exists());
397 assert!(!failed_dir.root().join(".git").exists());
398 assert!(!failed_dir.root().join(".jj").exists());
399
400 // Failed clone (if attempted) shouldn't remove the existing workspace
401 let output = root_dir.run_jj(["git", "clone", "--colocate", "bad", "clone"]);
402 insta::allow_duplicates! {
403 insta::assert_snapshot!(output, @r"
404 ------- stderr -------
405 Error: Destination path exists and is not an empty directory
406 [EOF]
407 [exit status: 1]
408 ");
409 }
410 assert!(clone_dir.root().join(".git").exists());
411 assert!(clone_dir.root().join(".jj").exists());
412
413 // Try cloning into an existing workspace
414 let output = root_dir.run_jj(["git", "clone", "source", "clone", "--colocate"]);
415 insta::allow_duplicates! {
416 insta::assert_snapshot!(output, @r"
417 ------- stderr -------
418 Error: Destination path exists and is not an empty directory
419 [EOF]
420 [exit status: 1]
421 ");
422 }
423
424 // Try cloning into an existing file
425 root_dir.write_file("file", "contents");
426 let output = root_dir.run_jj(["git", "clone", "source", "file", "--colocate"]);
427 insta::allow_duplicates! {
428 insta::assert_snapshot!(output, @r"
429 ------- stderr -------
430 Error: Destination path exists and is not an empty directory
431 [EOF]
432 [exit status: 1]
433 ");
434 }
435
436 // Try cloning into non-empty, non-workspace directory
437 clone_dir.remove_dir_all(".jj");
438 let output = root_dir.run_jj(["git", "clone", "source", "clone", "--colocate"]);
439 insta::allow_duplicates! {
440 insta::assert_snapshot!(output, @r"
441 ------- stderr -------
442 Error: Destination path exists and is not an empty directory
443 [EOF]
444 [exit status: 1]
445 ");
446 }
447
448 // Clone into a nested path
449 let output = root_dir.run_jj([
450 "git",
451 "clone",
452 "source",
453 "nested/path/to/repo",
454 "--colocate",
455 ]);
456 insta::allow_duplicates! {
457 insta::assert_snapshot!(output, @r#"
458 ------- stderr -------
459 Fetching into new repo in "$TEST_ENV/nested/path/to/repo"
460 bookmark: main@origin [new] tracked
461 Setting the revset alias `trunk()` to `main@origin`
462 Working copy (@) now at: vzqnnsmr 589d0921 (empty) (no description set)
463 Parent commit (@-) : qomsplrm ebeb70d8 main | message
464 Added 1 files, modified 0 files, removed 0 files
465 [EOF]
466 "#);
467 }
468}
469
470#[cfg_attr(feature = "git2", test_case(false; "use git2 for remote calls"))]
471#[test_case(true; "spawn a git subprocess for remote calls")]
472fn test_git_clone_remote_default_bookmark(subprocess: bool) {
473 let test_env = TestEnvironment::default();
474 let root_dir = test_env.work_dir("");
475 if !subprocess {
476 test_env.add_config("git.subprocess = false");
477 }
478 let git_repo_path = test_env.env_root().join("source");
479 let git_repo = git::init(git_repo_path.clone());
480
481 set_up_non_empty_git_repo(&git_repo);
482
483 // Create non-default bookmark in remote
484 let head_id = git_repo.head_id().unwrap().detach();
485 git_repo
486 .reference(
487 "refs/heads/feature1",
488 head_id,
489 gix::refs::transaction::PreviousValue::MustNotExist,
490 "",
491 )
492 .unwrap();
493
494 // All fetched bookmarks will be imported if auto-local-bookmark is on
495 test_env.add_config("git.auto-local-bookmark = true");
496 let output = root_dir.run_jj(["git", "clone", "source", "clone1"]);
497 insta::allow_duplicates! {
498 insta::assert_snapshot!(output, @r#"
499 ------- stderr -------
500 Fetching into new repo in "$TEST_ENV/clone1"
501 bookmark: feature1@origin [new] tracked
502 bookmark: main@origin [new] tracked
503 Setting the revset alias `trunk()` to `main@origin`
504 Working copy (@) now at: sqpuoqvx 2ca1c979 (empty) (no description set)
505 Parent commit (@-) : qomsplrm ebeb70d8 feature1 main | message
506 Added 1 files, modified 0 files, removed 0 files
507 [EOF]
508 "#);
509 }
510 let clone_dir1 = test_env.work_dir("clone1");
511 insta::allow_duplicates! {
512 insta::assert_snapshot!(get_bookmark_output(&clone_dir1), @r"
513 feature1: qomsplrm ebeb70d8 message
514 @origin: qomsplrm ebeb70d8 message
515 main: qomsplrm ebeb70d8 message
516 @origin: qomsplrm ebeb70d8 message
517 [EOF]
518 ");
519 }
520
521 // "trunk()" alias should be set to default bookmark "main"
522 let output = clone_dir1.run_jj(["config", "list", "--repo", "revset-aliases.'trunk()'"]);
523 insta::allow_duplicates! {
524 insta::assert_snapshot!(output, @r#"
525 revset-aliases.'trunk()' = "main@origin"
526 [EOF]
527 "#);
528 }
529
530 // Only the default bookmark will be imported if auto-local-bookmark is off
531 test_env.add_config("git.auto-local-bookmark = false");
532 let output = root_dir.run_jj(["git", "clone", "source", "clone2"]);
533 insta::allow_duplicates! {
534 insta::assert_snapshot!(output, @r#"
535 ------- stderr -------
536 Fetching into new repo in "$TEST_ENV/clone2"
537 bookmark: feature1@origin [new] untracked
538 bookmark: main@origin [new] untracked
539 Setting the revset alias `trunk()` to `main@origin`
540 Working copy (@) now at: rzvqmyuk 018092c2 (empty) (no description set)
541 Parent commit (@-) : qomsplrm ebeb70d8 feature1@origin main | message
542 Added 1 files, modified 0 files, removed 0 files
543 [EOF]
544 "#);
545 }
546 let clone_dir2 = test_env.work_dir("clone2");
547 insta::allow_duplicates! {
548 insta::assert_snapshot!(get_bookmark_output(&clone_dir2), @r"
549 feature1@origin: qomsplrm ebeb70d8 message
550 main: qomsplrm ebeb70d8 message
551 @origin: qomsplrm ebeb70d8 message
552 [EOF]
553 ");
554 }
555
556 // Change the default bookmark in remote
557 git::set_symbolic_reference(&git_repo, "HEAD", "refs/heads/feature1");
558 let output = root_dir.run_jj(["git", "clone", "source", "clone3"]);
559 insta::allow_duplicates! {
560 insta::assert_snapshot!(output, @r#"
561 ------- stderr -------
562 Fetching into new repo in "$TEST_ENV/clone3"
563 bookmark: feature1@origin [new] untracked
564 bookmark: main@origin [new] untracked
565 Setting the revset alias `trunk()` to `feature1@origin`
566 Working copy (@) now at: nppvrztz 5fd587f4 (empty) (no description set)
567 Parent commit (@-) : qomsplrm ebeb70d8 feature1 main@origin | message
568 Added 1 files, modified 0 files, removed 0 files
569 [EOF]
570 "#);
571 }
572 let clone_dir3 = test_env.work_dir("clone3");
573 insta::allow_duplicates! {
574 insta::assert_snapshot!(get_bookmark_output(&clone_dir3), @r"
575 feature1: qomsplrm ebeb70d8 message
576 @origin: qomsplrm ebeb70d8 message
577 main@origin: qomsplrm ebeb70d8 message
578 [EOF]
579 ");
580 }
581
582 // "trunk()" alias should be set to new default bookmark "feature1"
583 let output = clone_dir3.run_jj(["config", "list", "--repo", "revset-aliases.'trunk()'"]);
584 insta::allow_duplicates! {
585 insta::assert_snapshot!(output, @r#"
586 revset-aliases.'trunk()' = "feature1@origin"
587 [EOF]
588 "#);
589 }
590}
591
592// A branch with a strange name should get quoted in the config. Windows doesn't
593// like the strange name, so we don't run the test there.
594#[cfg(unix)]
595#[cfg_attr(feature = "git2", test_case(false; "use git2 for remote calls"))]
596#[test_case(true; "spawn a git subprocess for remote calls")]
597fn test_git_clone_remote_default_bookmark_with_escape(subprocess: bool) {
598 let test_env = TestEnvironment::default();
599 let root_dir = test_env.work_dir("");
600 if !subprocess {
601 test_env.add_config("git.subprocess = false");
602 }
603 let git_repo_path = test_env.env_root().join("source");
604 let git_repo = git::init(git_repo_path);
605 // Create a branch to something that needs to be escaped
606 let commit_id = git::add_commit(
607 &git_repo,
608 "refs/heads/\"",
609 "file",
610 b"content",
611 "message",
612 &[],
613 )
614 .commit_id;
615 git::set_head_to_id(&git_repo, commit_id);
616
617 let output = root_dir.run_jj(["git", "clone", "source", "clone"]);
618 insta::allow_duplicates! {
619 insta::assert_snapshot!(output, @r#"
620 ------- stderr -------
621 Fetching into new repo in "$TEST_ENV/clone"
622 bookmark: "\""@origin [new] untracked
623 Setting the revset alias `trunk()` to `"\""@origin`
624 Working copy (@) now at: sqpuoqvx 2ca1c979 (empty) (no description set)
625 Parent commit (@-) : qomsplrm ebeb70d8 " | message
626 Added 1 files, modified 0 files, removed 0 files
627 [EOF]
628 "#);
629 }
630
631 // "trunk()" alias should be escaped and quoted
632 let clone_dir = test_env.work_dir("clone");
633 let output = clone_dir.run_jj(["config", "list", "--repo", "revset-aliases.'trunk()'"]);
634 insta::allow_duplicates! {
635 insta::assert_snapshot!(output, @r#"
636 revset-aliases.'trunk()' = '"\""@origin'
637 [EOF]
638 "#);
639 }
640}
641
642#[cfg_attr(feature = "git2", test_case(false; "use git2 for remote calls"))]
643#[test_case(true; "spawn a git subprocess for remote calls")]
644fn test_git_clone_ignore_working_copy(subprocess: bool) {
645 let test_env = TestEnvironment::default();
646 let root_dir = test_env.work_dir("");
647 if !subprocess {
648 test_env.add_config("git.subprocess = false");
649 }
650 let git_repo_path = test_env.env_root().join("source");
651 let git_repo = git::init(git_repo_path);
652 set_up_non_empty_git_repo(&git_repo);
653
654 // Should not update working-copy files
655 let output = root_dir.run_jj(["git", "clone", "--ignore-working-copy", "source", "clone"]);
656 insta::allow_duplicates! {
657 insta::assert_snapshot!(output, @r#"
658 ------- stderr -------
659 Fetching into new repo in "$TEST_ENV/clone"
660 bookmark: main@origin [new] untracked
661 Setting the revset alias `trunk()` to `main@origin`
662 [EOF]
663 "#);
664 }
665 let clone_dir = test_env.work_dir("clone");
666
667 let output = clone_dir.run_jj(["status", "--ignore-working-copy"]);
668 insta::allow_duplicates! {
669 insta::assert_snapshot!(output, @r"
670 The working copy has no changes.
671 Working copy (@) : sqpuoqvx 2ca1c979 (empty) (no description set)
672 Parent commit (@-): qomsplrm ebeb70d8 main | message
673 [EOF]
674 ");
675 }
676
677 // TODO: Correct, but might be better to check out the root commit?
678 let output = clone_dir.run_jj(["status"]);
679 insta::allow_duplicates! {
680 insta::assert_snapshot!(output, @r"
681 ------- stderr -------
682 Error: The working copy is stale (not updated since operation eac759b9ab75).
683 Hint: Run `jj workspace update-stale` to update it.
684 See https://jj-vcs.github.io/jj/latest/working-copy/#stale-working-copy for more information.
685 [EOF]
686 [exit status: 1]
687 ");
688 }
689}
690
691#[cfg_attr(feature = "git2", test_case(false; "use git2 for remote calls"))]
692#[test_case(true; "spawn a git subprocess for remote calls")]
693fn test_git_clone_at_operation(subprocess: bool) {
694 let test_env = TestEnvironment::default();
695 let root_dir = test_env.work_dir("");
696 if !subprocess {
697 test_env.add_config("git.subprocess = false");
698 }
699 let git_repo_path = test_env.env_root().join("source");
700 let git_repo = git::init(git_repo_path);
701 set_up_non_empty_git_repo(&git_repo);
702
703 let output = root_dir.run_jj(["git", "clone", "--at-op=@-", "source", "clone"]);
704 insta::allow_duplicates! {
705 insta::assert_snapshot!(output, @r"
706 ------- stderr -------
707 Error: --at-op is not respected
708 [EOF]
709 [exit status: 2]
710 ");
711 }
712}
713
714#[cfg_attr(feature = "git2", test_case(false; "use git2 for remote calls"))]
715#[test_case(true; "spawn a git subprocess for remote calls")]
716fn test_git_clone_with_remote_name(subprocess: bool) {
717 let test_env = TestEnvironment::default();
718 let root_dir = test_env.work_dir("");
719 test_env.add_config("git.auto-local-bookmark = true");
720 if !subprocess {
721 test_env.add_config("git.subprocess = false");
722 }
723 let git_repo_path = test_env.env_root().join("source");
724 let git_repo = git::init(git_repo_path);
725 set_up_non_empty_git_repo(&git_repo);
726
727 // Clone with relative source path and a non-default remote name
728 let output = root_dir.run_jj(["git", "clone", "source", "clone", "--remote", "upstream"]);
729 insta::allow_duplicates! {
730 insta::assert_snapshot!(output, @r#"
731 ------- stderr -------
732 Fetching into new repo in "$TEST_ENV/clone"
733 bookmark: main@upstream [new] tracked
734 Setting the revset alias `trunk()` to `main@upstream`
735 Working copy (@) now at: sqpuoqvx 2ca1c979 (empty) (no description set)
736 Parent commit (@-) : qomsplrm ebeb70d8 main | message
737 Added 1 files, modified 0 files, removed 0 files
738 [EOF]
739 "#);
740 }
741}
742
743#[cfg_attr(feature = "git2", test_case(false; "use git2 for remote calls"))]
744#[test_case(true; "spawn a git subprocess for remote calls")]
745fn test_git_clone_with_remote_named_git(subprocess: bool) {
746 let test_env = TestEnvironment::default();
747 let root_dir = test_env.work_dir("");
748 if !subprocess {
749 test_env.add_config("git.subprocess = false");
750 }
751 let git_repo_path = test_env.env_root().join("source");
752 git::init(git_repo_path);
753
754 let output = root_dir.run_jj(["git", "clone", "--remote=git", "source", "dest"]);
755 insta::allow_duplicates! {
756 insta::assert_snapshot!(output, @r"
757 ------- stderr -------
758 Error: Git remote named 'git' is reserved for local Git repository
759 [EOF]
760 [exit status: 1]
761 ");
762 }
763}
764
765#[cfg_attr(feature = "git2", test_case(false; "use git2 for remote calls"))]
766#[test_case(true; "spawn a git subprocess for remote calls")]
767fn test_git_clone_with_remote_with_slashes(subprocess: bool) {
768 let test_env = TestEnvironment::default();
769 let root_dir = test_env.work_dir("");
770 if !subprocess {
771 test_env.add_config("git.subprocess = false");
772 }
773 let git_repo_path = test_env.env_root().join("source");
774 git::init(git_repo_path);
775
776 let output = root_dir.run_jj(["git", "clone", "--remote=slash/origin", "source", "dest"]);
777 insta::allow_duplicates! {
778 insta::assert_snapshot!(output, @r"
779 ------- stderr -------
780 Error: Git remotes with slashes are incompatible with jj: slash/origin
781 [EOF]
782 [exit status: 1]
783 ");
784 }
785}
786
787#[cfg_attr(feature = "git2", test_case(false; "use git2 for remote calls"))]
788#[test_case(true; "spawn a git subprocess for remote calls")]
789fn test_git_clone_trunk_deleted(subprocess: bool) {
790 let test_env = TestEnvironment::default();
791 let root_dir = test_env.work_dir("");
792 if !subprocess {
793 test_env.add_config("git.subprocess = false");
794 }
795 let git_repo_path = test_env.env_root().join("source");
796 let git_repo = git::init(git_repo_path);
797 set_up_non_empty_git_repo(&git_repo);
798 let clone_dir = test_env.work_dir("clone");
799
800 let output = root_dir.run_jj(["git", "clone", "source", "clone"]);
801 insta::allow_duplicates! {
802 insta::assert_snapshot!(output, @r#"
803 ------- stderr -------
804 Fetching into new repo in "$TEST_ENV/clone"
805 bookmark: main@origin [new] untracked
806 Setting the revset alias `trunk()` to `main@origin`
807 Working copy (@) now at: sqpuoqvx 2ca1c979 (empty) (no description set)
808 Parent commit (@-) : qomsplrm ebeb70d8 main | message
809 Added 1 files, modified 0 files, removed 0 files
810 [EOF]
811 "#);
812 }
813
814 let output = clone_dir.run_jj(["bookmark", "forget", "--include-remotes", "main"]);
815 insta::allow_duplicates! {
816 insta::assert_snapshot!(output, @r"
817 ------- stderr -------
818 Forgot 1 local bookmarks.
819 Forgot 1 remote bookmarks.
820 Warning: Failed to resolve `revset-aliases.trunk()`: Revision `main@origin` doesn't exist
821 Hint: Use `jj config edit --repo` to adjust the `trunk()` alias.
822 [EOF]
823 ");
824 }
825
826 let output = clone_dir.run_jj(["log"]);
827 insta::allow_duplicates! {
828 insta::assert_snapshot!(output, @r"
829 @ sqpuoqvx test.user@example.com 2001-02-03 08:05:07 2ca1c979
830 │ (empty) (no description set)
831 ○ qomsplrm someone@example.org 1970-01-01 11:00:00 ebeb70d8
832 │ message
833 ◆ zzzzzzzz root() 00000000
834 [EOF]
835 ------- stderr -------
836 Warning: Failed to resolve `revset-aliases.trunk()`: Revision `main@origin` doesn't exist
837 Hint: Use `jj config edit --repo` to adjust the `trunk()` alias.
838 [EOF]
839 ");
840 }
841}
842
843#[test]
844fn test_git_clone_conditional_config() {
845 let test_env = TestEnvironment::default();
846 let root_dir = test_env.work_dir("");
847 let source_repo_path = test_env.env_root().join("source");
848 let old_workspace_dir = test_env.work_dir("old");
849 let new_workspace_dir = test_env.work_dir("new");
850 let source_git_repo = git::init(source_repo_path);
851 set_up_non_empty_git_repo(&source_git_repo);
852
853 let run_jj = |work_dir: &TestWorkDir, args: &[&str]| {
854 work_dir.run_jj_with(|cmd| {
855 cmd.args(args)
856 .env_remove("JJ_EMAIL")
857 .env_remove("JJ_OP_HOSTNAME")
858 .env_remove("JJ_OP_USERNAME")
859 })
860 };
861 let log_template = r#"separate(' ', author.email(), description.first_line()) ++ "\n""#;
862 let op_log_template = r#"separate(' ', user, description.first_line()) ++ "\n""#;
863
864 // Override user.email and operation.username conditionally
865 test_env.add_config(formatdoc! {"
866 user.email = 'base@example.org'
867 operation.hostname = 'base'
868 operation.username = 'base'
869 [[--scope]]
870 --when.repositories = [{new_workspace_root}]
871 user.email = 'new-repo@example.org'
872 operation.username = 'new-repo'
873 ",
874 new_workspace_root = to_toml_value(new_workspace_dir.root().to_str().unwrap()),
875 });
876
877 // Override operation.hostname by repo config, which should be loaded into
878 // the command settings, but shouldn't be copied to the new repo.
879 run_jj(&root_dir, &["git", "init", "old"]).success();
880 run_jj(
881 &old_workspace_dir,
882 &["config", "set", "--repo", "operation.hostname", "old-repo"],
883 )
884 .success();
885 run_jj(&old_workspace_dir, &["new"]).success();
886 let output = run_jj(&old_workspace_dir, &["op", "log", "-T", op_log_template]);
887 insta::assert_snapshot!(output, @r"
888 @ base@old-repo new empty commit
889 ○ base@base add workspace 'default'
890 ○ @
891 [EOF]
892 ");
893
894 // Clone repo at the old workspace directory.
895 let output = run_jj(&old_workspace_dir, &["git", "clone", "../source", "../new"]);
896 insta::assert_snapshot!(output, @r#"
897 ------- stderr -------
898 Fetching into new repo in "$TEST_ENV/new"
899 bookmark: main@origin [new] untracked
900 Setting the revset alias `trunk()` to `main@origin`
901 Working copy (@) now at: zxsnswpr 9ffb42e2 (empty) (no description set)
902 Parent commit (@-) : qomsplrm ebeb70d8 main | message
903 Added 1 files, modified 0 files, removed 0 files
904 [EOF]
905 "#);
906 run_jj(&new_workspace_dir, &["new"]).success();
907 let output = run_jj(&new_workspace_dir, &["log", "-T", log_template]);
908 insta::assert_snapshot!(output, @r"
909 @ new-repo@example.org
910 ○ new-repo@example.org
911 ◆ someone@example.org message
912 │
913 ~
914 [EOF]
915 ");
916 let output = run_jj(&new_workspace_dir, &["op", "log", "-T", op_log_template]);
917 insta::assert_snapshot!(output, @r"
918 @ new-repo@base new empty commit
919 ○ new-repo@base check out git remote's default branch
920 ○ new-repo@base fetch from git remote into empty repo
921 ○ new-repo@base add workspace 'default'
922 ○ @
923 [EOF]
924 ");
925}
926
927#[cfg(feature = "git2")]
928#[test]
929fn test_git_clone_with_depth_git2() {
930 let test_env = TestEnvironment::default();
931 let root_dir = test_env.work_dir("");
932 test_env.add_config("git.auto-local-bookmark = true");
933 test_env.add_config("git.subprocess = false");
934 let git_repo_path = test_env.env_root().join("source");
935 let git_repo = git::init(git_repo_path);
936 set_up_non_empty_git_repo(&git_repo);
937
938 // git does support shallow clones on the local transport, so it will work
939 // (we cannot replicate git2's erroneous behaviour wrt git)
940 // local transport does not support shallow clones so we just test that the
941 // depth arg is passed on here
942 let output = root_dir.run_jj(["git", "clone", "--depth", "1", "source", "clone"]);
943 insta::assert_snapshot!(output, @r#"
944 ------- stderr -------
945 Fetching into new repo in "$TEST_ENV/clone"
946 Error: shallow fetch is not supported by the local transport; class=Net (12)
947 [EOF]
948 [exit status: 1]
949 "#);
950}
951
952#[test]
953fn test_git_clone_with_depth_subprocess() {
954 let test_env = TestEnvironment::default();
955 let root_dir = test_env.work_dir("");
956 test_env.add_config("git.auto-local-bookmark = true");
957 let clone_dir = test_env.work_dir("clone");
958 let git_repo_path = test_env.env_root().join("source");
959 let git_repo = git::init(git_repo_path);
960 set_up_non_empty_git_repo(&git_repo);
961
962 // git does support shallow clones on the local transport, so it will work
963 // (we cannot replicate git2's erroneous behaviour wrt git)
964 let output = root_dir.run_jj(["git", "clone", "--depth", "1", "source", "clone"]);
965 insta::assert_snapshot!(output, @r#"
966 ------- stderr -------
967 Fetching into new repo in "$TEST_ENV/clone"
968 bookmark: main@origin [new] tracked
969 Setting the revset alias `trunk()` to `main@origin`
970 Working copy (@) now at: sqpuoqvx 2ca1c979 (empty) (no description set)
971 Parent commit (@-) : qomsplrm ebeb70d8 main | message
972 Added 1 files, modified 0 files, removed 0 files
973 [EOF]
974 "#);
975
976 let output = clone_dir.run_jj(["log"]);
977 insta::assert_snapshot!(output, @r"
978 @ sqpuoqvx test.user@example.com 2001-02-03 08:05:07 2ca1c979
979 │ (empty) (no description set)
980 ◆ qomsplrm someone@example.org 1970-01-01 11:00:00 main ebeb70d8
981 │ message
982 ~
983 [EOF]
984 ");
985}
986
987#[cfg_attr(feature = "git2", test_case(false; "use git2 for remote calls"))]
988#[test_case(true; "spawn a git subprocess for remote calls")]
989fn test_git_clone_invalid_immutable_heads(subprocess: bool) {
990 let test_env = TestEnvironment::default();
991 let root_dir = test_env.work_dir("");
992 if !subprocess {
993 test_env.add_config("git.subprocess = false");
994 }
995 let git_repo_path = test_env.env_root().join("source");
996 let git_repo = git::init(git_repo_path);
997 set_up_non_empty_git_repo(&git_repo);
998
999 test_env.add_config("revset-aliases.'immutable_heads()' = 'unknown'");
1000 // Suppress lengthy warnings in commit summary template
1001 test_env.add_config("revsets.short-prefixes = ''");
1002
1003 // The error shouldn't be counted as an immutable working-copy commit. It
1004 // should be reported.
1005 let output = root_dir.run_jj(["git", "clone", "source", "clone"]);
1006 insta::allow_duplicates! {
1007 insta::assert_snapshot!(output, @r#"
1008 ------- stderr -------
1009 Fetching into new repo in "$TEST_ENV/clone"
1010 bookmark: main@origin [new] untracked
1011 Config error: Invalid `revset-aliases.immutable_heads()`
1012 Caused by: Revision `unknown` doesn't exist
1013 For help, see https://jj-vcs.github.io/jj/latest/config/ or use `jj help -k config`.
1014 [EOF]
1015 [exit status: 1]
1016 "#);
1017 }
1018}
1019
1020#[cfg_attr(feature = "git2", test_case(false; "use git2 for remote calls"))]
1021#[test_case(true; "spawn a git subprocess for remote calls")]
1022fn test_git_clone_malformed(subprocess: bool) {
1023 let test_env = TestEnvironment::default();
1024 let root_dir = test_env.work_dir("");
1025 if !subprocess {
1026 test_env.add_config("git.subprocess = false");
1027 }
1028 let git_repo_path = test_env.env_root().join("source");
1029 let git_repo = git::init(git_repo_path);
1030 let clone_dir = test_env.work_dir("clone");
1031 // we can insert ".jj" entry to create a malformed clone
1032 set_up_git_repo_with_file(&git_repo, ".jj");
1033
1034 // TODO: Perhaps, this should be a user error, not an internal error.
1035 let output = root_dir.run_jj(["git", "clone", "source", "clone"]);
1036 insta::allow_duplicates! {
1037 insta::assert_snapshot!(output, @r#"
1038 ------- stderr -------
1039 Fetching into new repo in "$TEST_ENV/clone"
1040 bookmark: main@origin [new] untracked
1041 Setting the revset alias `trunk()` to `main@origin`
1042 Internal error: Failed to check out commit 0a09cb41583450703459a2310d63da61456364ce
1043 Caused by: Reserved path component .jj in $TEST_ENV/clone/.jj
1044 [EOF]
1045 [exit status: 255]
1046 "#);
1047 }
1048
1049 // The cloned workspace isn't usable.
1050 let output = clone_dir.run_jj(["status"]);
1051 insta::allow_duplicates! {
1052 insta::assert_snapshot!(output, @r"
1053 ------- stderr -------
1054 Error: The working copy is stale (not updated since operation 57e024eb3edf).
1055 Hint: Run `jj workspace update-stale` to update it.
1056 See https://jj-vcs.github.io/jj/latest/working-copy/#stale-working-copy for more information.
1057 [EOF]
1058 [exit status: 1]
1059 ");
1060 }
1061
1062 // The error can be somehow recovered.
1063 // TODO: add an update-stale flag to reset the working-copy?
1064 let output = clone_dir.run_jj(["workspace", "update-stale"]);
1065 insta::allow_duplicates! {
1066 insta::assert_snapshot!(output, @r"
1067 ------- stderr -------
1068 Internal error: Failed to check out commit 0a09cb41583450703459a2310d63da61456364ce
1069 Caused by: Reserved path component .jj in $TEST_ENV/clone/.jj
1070 [EOF]
1071 [exit status: 255]
1072 ");
1073 }
1074 let output = clone_dir.run_jj(["new", "root()", "--ignore-working-copy"]);
1075 insta::allow_duplicates! {
1076 insta::assert_snapshot!(output, @"");
1077 }
1078 let output = clone_dir.run_jj(["status"]);
1079 insta::allow_duplicates! {
1080 insta::assert_snapshot!(output, @r"
1081 The working copy has no changes.
1082 Working copy (@) : zsuskuln f652c321 (empty) (no description set)
1083 Parent commit (@-): zzzzzzzz 00000000 (empty) (no description set)
1084 [EOF]
1085 ");
1086 }
1087}
1088
1089#[test]
1090fn test_git_clone_no_git_executable() {
1091 let test_env = TestEnvironment::default();
1092 let root_dir = test_env.work_dir("");
1093 test_env.add_config("git.executable-path = 'jj-test-missing-program'");
1094 let git_repo_path = test_env.env_root().join("source");
1095 let git_repo = git::init(git_repo_path);
1096 set_up_non_empty_git_repo(&git_repo);
1097
1098 let output = root_dir.run_jj(["git", "clone", "source", "clone"]);
1099 insta::assert_snapshot!(output.strip_stderr_last_line(), @r#"
1100 ------- stderr -------
1101 Fetching into new repo in "$TEST_ENV/clone"
1102 Error: Could not execute the git process, found in the OS path 'jj-test-missing-program'
1103 [EOF]
1104 [exit status: 1]
1105 "#);
1106}
1107
1108#[test]
1109fn test_git_clone_no_git_executable_with_path() {
1110 let test_env = TestEnvironment::default();
1111 let root_dir = test_env.work_dir("");
1112 let invalid_git_executable_path = test_env.env_root().join("invalid").join("path");
1113 test_env.add_config(format!(
1114 "git.executable-path = {}",
1115 to_toml_value(invalid_git_executable_path.to_str().unwrap())
1116 ));
1117 let git_repo_path = test_env.env_root().join("source");
1118 let git_repo = git::init(git_repo_path);
1119 set_up_non_empty_git_repo(&git_repo);
1120
1121 let output = root_dir.run_jj(["git", "clone", "source", "clone"]);
1122 insta::assert_snapshot!(output.strip_stderr_last_line(), @r#"
1123 ------- stderr -------
1124 Fetching into new repo in "$TEST_ENV/clone"
1125 Error: Could not execute git process at specified path '$TEST_ENV/invalid/path'
1126 [EOF]
1127 [exit status: 1]
1128 "#);
1129}
1130
1131#[must_use]
1132fn get_bookmark_output(work_dir: &TestWorkDir) -> CommandOutput {
1133 work_dir.run_jj(["bookmark", "list", "--all-remotes"])
1134}
1135
1136// TODO: Remove with the `git.subprocess` setting.
1137#[cfg(not(feature = "git2"))]
1138#[test]
1139fn test_git_clone_git2_warning() {
1140 let test_env = TestEnvironment::default();
1141 let root_dir = test_env.work_dir("");
1142 test_env.add_config("git.subprocess = false");
1143 test_env.add_config("git.auto-local-bookmark = true");
1144 let git_repo_path = test_env.env_root().join("source");
1145 let git_repo = git::init(git_repo_path);
1146
1147 set_up_non_empty_git_repo(&git_repo);
1148
1149 let output = root_dir.run_jj(["git", "clone", "source", "clone"]);
1150 insta::assert_snapshot!(output, @r#"
1151 ------- stderr -------
1152 Warning: Deprecated config: jj was compiled without `git.subprocess = false` support
1153 Fetching into new repo in "$TEST_ENV/clone"
1154 bookmark: main@origin [new] tracked
1155 Setting the revset alias `trunk()` to `main@origin`
1156 Working copy (@) now at: sqpuoqvx 2ca1c979 (empty) (no description set)
1157 Parent commit (@-) : qomsplrm ebeb70d8 main | message
1158 Added 1 files, modified 0 files, removed 0 files
1159 [EOF]
1160 "#);
1161}