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