just playing with tangled
1// Copyright 2023 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 test_case::test_case;
16use testutils::git;
17
18use crate::common::create_commit;
19use crate::common::CommandOutput;
20use crate::common::TestEnvironment;
21use crate::common::TestWorkDir;
22
23fn add_commit_to_branch(git_repo: &gix::Repository, branch: &str) -> gix::ObjectId {
24 git::add_commit(
25 git_repo,
26 &format!("refs/heads/{branch}"),
27 branch, // filename
28 branch.as_bytes(), // content
29 "message",
30 &[],
31 )
32 .commit_id
33}
34
35/// Creates a remote Git repo containing a bookmark with the same name
36fn init_git_remote(test_env: &TestEnvironment, remote: &str) -> gix::Repository {
37 let git_repo_path = test_env.env_root().join(remote);
38 let git_repo = git::init(git_repo_path);
39 add_commit_to_branch(&git_repo, remote);
40
41 git_repo
42}
43
44/// Add a remote containing a bookmark with the same name
45fn add_git_remote(
46 test_env: &TestEnvironment,
47 work_dir: &TestWorkDir,
48 remote: &str,
49) -> gix::Repository {
50 let repo = init_git_remote(test_env, remote);
51 work_dir
52 .run_jj(["git", "remote", "add", remote, &format!("../{remote}")])
53 .success();
54
55 repo
56}
57
58#[must_use]
59fn get_bookmark_output(work_dir: &TestWorkDir) -> CommandOutput {
60 // --quiet to suppress deleted bookmarks hint
61 work_dir.run_jj(["bookmark", "list", "--all-remotes", "--quiet"])
62}
63
64#[must_use]
65fn get_log_output(work_dir: &TestWorkDir) -> CommandOutput {
66 let template =
67 r#"commit_id.short() ++ " \"" ++ description.first_line() ++ "\" " ++ bookmarks"#;
68 work_dir.run_jj(["log", "-T", template, "-r", "all()"])
69}
70
71fn clone_git_remote_into(
72 test_env: &TestEnvironment,
73 upstream: &str,
74 fork: &str,
75) -> gix::Repository {
76 let upstream_path = test_env.env_root().join(upstream);
77 let fork_path = test_env.env_root().join(fork);
78 let fork_repo = git::clone(&fork_path, upstream_path.to_str().unwrap(), Some(upstream));
79
80 // create local branch mirroring the upstream
81 let upstream_head = fork_repo
82 .find_reference(&format!("refs/remotes/{upstream}/{upstream}"))
83 .unwrap()
84 .peel_to_id_in_place()
85 .unwrap()
86 .detach();
87
88 fork_repo
89 .reference(
90 format!("refs/heads/{upstream}"),
91 upstream_head,
92 gix::refs::transaction::PreviousValue::MustNotExist,
93 "create tracking head",
94 )
95 .unwrap();
96
97 fork_repo
98}
99
100#[cfg_attr(feature = "git2", test_case(false; "use git2 for remote calls"))]
101#[test_case(true; "spawn a git subprocess for remote calls")]
102fn test_git_fetch_with_default_config(subprocess: bool) {
103 let test_env = TestEnvironment::default();
104 if !subprocess {
105 test_env.add_config("git.subprocess = false");
106 }
107 test_env.run_jj_in(".", ["git", "init", "repo"]).success();
108 let work_dir = test_env.work_dir("repo");
109 add_git_remote(&test_env, &work_dir, "origin");
110
111 work_dir.run_jj(["git", "fetch"]).success();
112 insta::allow_duplicates! {
113 insta::assert_snapshot!(get_bookmark_output(&work_dir), @r"
114 origin@origin: qmyrypzk ab8b299e message
115 [EOF]
116 ");
117 }
118}
119
120#[cfg_attr(feature = "git2", test_case(false; "use git2 for remote calls"))]
121#[test_case(true; "spawn a git subprocess for remote calls")]
122fn test_git_fetch_default_remote(subprocess: bool) {
123 let test_env = TestEnvironment::default();
124 if !subprocess {
125 test_env.add_config("git.subprocess = false");
126 }
127 test_env.add_config("git.auto-local-bookmark = true");
128 test_env.run_jj_in(".", ["git", "init", "repo"]).success();
129 let work_dir = test_env.work_dir("repo");
130 add_git_remote(&test_env, &work_dir, "origin");
131
132 work_dir.run_jj(["git", "fetch"]).success();
133 insta::allow_duplicates! {
134 insta::assert_snapshot!(get_bookmark_output(&work_dir), @r"
135 origin: qmyrypzk ab8b299e message
136 @origin: qmyrypzk ab8b299e message
137 [EOF]
138 ");
139 }
140}
141
142#[cfg_attr(feature = "git2", test_case(false; "use git2 for remote calls"))]
143#[test_case(true; "spawn a git subprocess for remote calls")]
144fn test_git_fetch_single_remote(subprocess: bool) {
145 let test_env = TestEnvironment::default();
146 if !subprocess {
147 test_env.add_config("git.subprocess = false");
148 }
149 test_env.add_config("git.auto-local-bookmark = true");
150 test_env.run_jj_in(".", ["git", "init", "repo"]).success();
151 let work_dir = test_env.work_dir("repo");
152 add_git_remote(&test_env, &work_dir, "rem1");
153
154 let output = work_dir.run_jj(["git", "fetch"]);
155 insta::allow_duplicates! {
156 insta::assert_snapshot!(output, @r"
157 ------- stderr -------
158 Hint: Fetching from the only existing remote: rem1
159 bookmark: rem1@rem1 [new] tracked
160 [EOF]
161 ");
162 }
163 insta::allow_duplicates! {
164 insta::assert_snapshot!(get_bookmark_output(&work_dir), @r"
165 rem1: ppspxspk 4acd0343 message
166 @rem1: ppspxspk 4acd0343 message
167 [EOF]
168 ");
169 }
170}
171
172#[cfg_attr(feature = "git2", test_case(false; "use git2 for remote calls"))]
173#[test_case(true; "spawn a git subprocess for remote calls")]
174fn test_git_fetch_single_remote_all_remotes_flag(subprocess: bool) {
175 let test_env = TestEnvironment::default();
176 if !subprocess {
177 test_env.add_config("git.subprocess = false");
178 }
179 test_env.add_config("git.auto-local-bookmark = true");
180 test_env.run_jj_in(".", ["git", "init", "repo"]).success();
181 let work_dir = test_env.work_dir("repo");
182 add_git_remote(&test_env, &work_dir, "rem1");
183
184 work_dir.run_jj(["git", "fetch", "--all-remotes"]).success();
185 insta::allow_duplicates! {
186 insta::assert_snapshot!(get_bookmark_output(&work_dir), @r"
187 rem1: ppspxspk 4acd0343 message
188 @rem1: ppspxspk 4acd0343 message
189 [EOF]
190 ");
191 }
192}
193
194#[cfg_attr(feature = "git2", test_case(false; "use git2 for remote calls"))]
195#[test_case(true; "spawn a git subprocess for remote calls")]
196fn test_git_fetch_single_remote_from_arg(subprocess: bool) {
197 let test_env = TestEnvironment::default();
198 if !subprocess {
199 test_env.add_config("git.subprocess = false");
200 }
201 test_env.add_config("git.auto-local-bookmark = true");
202 test_env.run_jj_in(".", ["git", "init", "repo"]).success();
203 let work_dir = test_env.work_dir("repo");
204 add_git_remote(&test_env, &work_dir, "rem1");
205
206 work_dir
207 .run_jj(["git", "fetch", "--remote", "rem1"])
208 .success();
209 insta::allow_duplicates! {
210 insta::assert_snapshot!(get_bookmark_output(&work_dir), @r"
211 rem1: ppspxspk 4acd0343 message
212 @rem1: ppspxspk 4acd0343 message
213 [EOF]
214 ");
215 }
216}
217
218#[cfg_attr(feature = "git2", test_case(false; "use git2 for remote calls"))]
219#[test_case(true; "spawn a git subprocess for remote calls")]
220fn test_git_fetch_single_remote_from_config(subprocess: bool) {
221 let test_env = TestEnvironment::default();
222 if !subprocess {
223 test_env.add_config("git.subprocess = false");
224 }
225 test_env.add_config("git.auto-local-bookmark = true");
226 test_env.run_jj_in(".", ["git", "init", "repo"]).success();
227 let work_dir = test_env.work_dir("repo");
228 add_git_remote(&test_env, &work_dir, "rem1");
229 test_env.add_config(r#"git.fetch = "rem1""#);
230
231 work_dir.run_jj(["git", "fetch"]).success();
232 insta::allow_duplicates! {
233 insta::assert_snapshot!(get_bookmark_output(&work_dir), @r"
234 rem1: ppspxspk 4acd0343 message
235 @rem1: ppspxspk 4acd0343 message
236 [EOF]
237 ");
238 }
239}
240
241#[cfg_attr(feature = "git2", test_case(false; "use git2 for remote calls"))]
242#[test_case(true; "spawn a git subprocess for remote calls")]
243fn test_git_fetch_multiple_remotes(subprocess: bool) {
244 let test_env = TestEnvironment::default();
245 if !subprocess {
246 test_env.add_config("git.subprocess = false");
247 }
248 test_env.add_config("git.auto-local-bookmark = true");
249 test_env.run_jj_in(".", ["git", "init", "repo"]).success();
250 let work_dir = test_env.work_dir("repo");
251 add_git_remote(&test_env, &work_dir, "rem1");
252 add_git_remote(&test_env, &work_dir, "rem2");
253
254 work_dir
255 .run_jj(["git", "fetch", "--remote", "rem1", "--remote", "rem2"])
256 .success();
257 insta::allow_duplicates! {
258 insta::assert_snapshot!(get_bookmark_output(&work_dir), @r"
259 rem1: ppspxspk 4acd0343 message
260 @rem1: ppspxspk 4acd0343 message
261 rem2: pzqqpnpo 44c57802 message
262 @rem2: pzqqpnpo 44c57802 message
263 [EOF]
264 ");
265 }
266}
267
268#[cfg_attr(feature = "git2", test_case(false; "use git2 for remote calls"))]
269#[test_case(true; "spawn a git subprocess for remote calls")]
270fn test_git_fetch_with_glob(subprocess: bool) {
271 let test_env = TestEnvironment::default();
272 if !subprocess {
273 test_env.add_config("git.subprocess = false");
274 }
275 test_env.run_jj_in(".", ["git", "init", "repo"]).success();
276 let work_dir = test_env.work_dir("repo");
277 add_git_remote(&test_env, &work_dir, "rem1");
278 add_git_remote(&test_env, &work_dir, "rem2");
279
280 let output = work_dir.run_jj(["git", "fetch", "--remote", "glob:*"]);
281 insta::allow_duplicates! {
282 insta::assert_snapshot!(output, @r"
283 ------- stderr -------
284 bookmark: rem1@rem1 [new] untracked
285 bookmark: rem2@rem2 [new] untracked
286 [EOF]
287 ");
288 }
289}
290
291#[cfg_attr(feature = "git2", test_case(false; "use git2 for remote calls"))]
292#[test_case(true; "spawn a git subprocess for remote calls")]
293fn test_git_fetch_with_glob_and_exact_match(subprocess: bool) {
294 let test_env = TestEnvironment::default();
295 if !subprocess {
296 test_env.add_config("git.subprocess = false");
297 }
298 test_env.run_jj_in(".", ["git", "init", "repo"]).success();
299 let work_dir = test_env.work_dir("repo");
300 add_git_remote(&test_env, &work_dir, "rem1");
301 add_git_remote(&test_env, &work_dir, "rem2");
302 add_git_remote(&test_env, &work_dir, "upstream1");
303 add_git_remote(&test_env, &work_dir, "upstream2");
304 add_git_remote(&test_env, &work_dir, "origin");
305
306 let output = work_dir.run_jj(["git", "fetch", "--remote=glob:rem*", "--remote=origin"]);
307 insta::allow_duplicates! {
308 insta::assert_snapshot!(output, @r"
309 ------- stderr -------
310 bookmark: origin@origin [new] untracked
311 bookmark: rem1@rem1 [new] untracked
312 bookmark: rem2@rem2 [new] untracked
313 [EOF]
314 ");
315 }
316}
317
318#[cfg_attr(feature = "git2", test_case(false; "use git2 for remote calls"))]
319#[test_case(true; "spawn a git subprocess for remote calls")]
320fn test_git_fetch_with_glob_from_config(subprocess: bool) {
321 let test_env = TestEnvironment::default();
322 if !subprocess {
323 test_env.add_config("git.subprocess = false");
324 }
325 test_env.add_config(r#"git.fetch = "glob:rem*""#);
326 test_env.run_jj_in(".", ["git", "init", "repo"]).success();
327 let work_dir = test_env.work_dir("repo");
328 add_git_remote(&test_env, &work_dir, "rem1");
329 add_git_remote(&test_env, &work_dir, "rem2");
330 add_git_remote(&test_env, &work_dir, "upstream");
331
332 let output = work_dir.run_jj(["git", "fetch"]);
333 insta::allow_duplicates! {
334 insta::assert_snapshot!(output, @r"
335 ------- stderr -------
336 bookmark: rem1@rem1 [new] untracked
337 bookmark: rem2@rem2 [new] untracked
338 [EOF]
339 ");
340 }
341}
342
343#[cfg_attr(feature = "git2", test_case(false; "use git2 for remote calls"))]
344#[test_case(true; "spawn a git subprocess for remote calls")]
345fn test_git_fetch_with_glob_with_no_matching_remotes(subprocess: bool) {
346 let test_env = TestEnvironment::default();
347 if !subprocess {
348 test_env.add_config("git.subprocess = false");
349 }
350 test_env.run_jj_in(".", ["git", "init", "repo"]).success();
351 let work_dir = test_env.work_dir("repo");
352 add_git_remote(&test_env, &work_dir, "upstream");
353
354 let output = work_dir.run_jj(["git", "fetch", "--remote=glob:rem*"]);
355 insta::allow_duplicates! {
356 insta::assert_snapshot!(output, @r"
357 ------- stderr -------
358 Error: No matching git remotes for patterns: rem*
359 [EOF]
360 [exit status: 1]
361 ");
362 }
363 // No remote should have been fetched as part of the failing transaction
364 insta::allow_duplicates! {
365 insta::assert_snapshot!(get_bookmark_output(&work_dir), @"");
366 }
367}
368
369#[cfg_attr(feature = "git2", test_case(false; "use git2 for remote calls"))]
370#[test_case(true; "spawn a git subprocess for remote calls")]
371fn test_git_fetch_all_remotes(subprocess: bool) {
372 let test_env = TestEnvironment::default();
373 if !subprocess {
374 test_env.add_config("git.subprocess = false");
375 }
376 test_env.add_config("git.auto-local-bookmark = true");
377 test_env.run_jj_in(".", ["git", "init", "repo"]).success();
378 let work_dir = test_env.work_dir("repo");
379 add_git_remote(&test_env, &work_dir, "rem1");
380 add_git_remote(&test_env, &work_dir, "rem2");
381
382 // add empty [remote "rem3"] section to .git/config, which should be ignored
383 work_dir
384 .run_jj(["git", "remote", "add", "rem3", "../unknown"])
385 .success();
386 work_dir
387 .run_jj(["git", "remote", "remove", "rem3"])
388 .success();
389
390 work_dir.run_jj(["git", "fetch", "--all-remotes"]).success();
391 insta::allow_duplicates! {
392 insta::assert_snapshot!(get_bookmark_output(&work_dir), @r"
393 rem1: ppspxspk 4acd0343 message
394 @rem1: ppspxspk 4acd0343 message
395 rem2: pzqqpnpo 44c57802 message
396 @rem2: pzqqpnpo 44c57802 message
397 [EOF]
398 ");
399 }
400}
401
402#[cfg_attr(feature = "git2", test_case(false; "use git2 for remote calls"))]
403#[test_case(true; "spawn a git subprocess for remote calls")]
404fn test_git_fetch_multiple_remotes_from_config(subprocess: bool) {
405 let test_env = TestEnvironment::default();
406 if !subprocess {
407 test_env.add_config("git.subprocess = false");
408 }
409 test_env.add_config("git.auto-local-bookmark = true");
410 test_env.run_jj_in(".", ["git", "init", "repo"]).success();
411 let work_dir = test_env.work_dir("repo");
412 add_git_remote(&test_env, &work_dir, "rem1");
413 add_git_remote(&test_env, &work_dir, "rem2");
414 test_env.add_config(r#"git.fetch = ["rem1", "rem2"]"#);
415
416 work_dir.run_jj(["git", "fetch"]).success();
417 insta::allow_duplicates! {
418 insta::assert_snapshot!(get_bookmark_output(&work_dir), @r"
419 rem1: ppspxspk 4acd0343 message
420 @rem1: ppspxspk 4acd0343 message
421 rem2: pzqqpnpo 44c57802 message
422 @rem2: pzqqpnpo 44c57802 message
423 [EOF]
424 ");
425 }
426}
427
428#[cfg_attr(feature = "git2", test_case(false; "use git2 for remote calls"))]
429#[test_case(true; "spawn a git subprocess for remote calls")]
430fn test_git_fetch_nonexistent_remote(subprocess: bool) {
431 let test_env = TestEnvironment::default();
432 if !subprocess {
433 test_env.add_config("git.subprocess = false");
434 }
435 test_env.run_jj_in(".", ["git", "init", "repo"]).success();
436 let work_dir = test_env.work_dir("repo");
437 add_git_remote(&test_env, &work_dir, "rem1");
438
439 let output = work_dir.run_jj(["git", "fetch", "--remote", "rem1", "--remote", "rem2"]);
440 insta::allow_duplicates! {
441 insta::assert_snapshot!(output, @r"
442 ------- stderr -------
443 Error: No git remote named 'rem2'
444 [EOF]
445 [exit status: 1]
446 ");
447 }
448 insta::allow_duplicates! {
449 // No remote should have been fetched as part of the failing transaction
450 insta::assert_snapshot!(get_bookmark_output(&work_dir), @"");
451 }
452}
453
454#[cfg_attr(feature = "git2", test_case(false; "use git2 for remote calls"))]
455#[test_case(true; "spawn a git subprocess for remote calls")]
456fn test_git_fetch_nonexistent_remote_from_config(subprocess: bool) {
457 let test_env = TestEnvironment::default();
458 if !subprocess {
459 test_env.add_config("git.subprocess = false");
460 }
461 test_env.run_jj_in(".", ["git", "init", "repo"]).success();
462 let work_dir = test_env.work_dir("repo");
463 add_git_remote(&test_env, &work_dir, "rem1");
464 test_env.add_config(r#"git.fetch = ["rem1", "rem2"]"#);
465
466 let output = work_dir.run_jj(["git", "fetch"]);
467 insta::allow_duplicates! {
468 insta::assert_snapshot!(output, @r"
469 ------- stderr -------
470 Error: No git remote named 'rem2'
471 [EOF]
472 [exit status: 1]
473 ");
474 }
475 // No remote should have been fetched as part of the failing transaction
476 insta::allow_duplicates! {
477 insta::assert_snapshot!(get_bookmark_output(&work_dir), @"");
478 }
479}
480
481#[cfg_attr(feature = "git2", test_case(false; "use git2 for remote calls"))]
482#[test_case(true; "spawn a git subprocess for remote calls")]
483fn test_git_fetch_from_remote_named_git(subprocess: bool) {
484 let test_env = TestEnvironment::default();
485 if !subprocess {
486 test_env.add_config("git.subprocess = false");
487 }
488 test_env.add_config("git.auto-local-bookmark = true");
489 let work_dir = test_env.work_dir("repo");
490 init_git_remote(&test_env, "git");
491
492 git::init(work_dir.root());
493 git::add_remote(work_dir.root(), "git", "../git");
494
495 // Existing remote named 'git' shouldn't block the repo initialization.
496 work_dir.run_jj(["git", "init", "--git-repo=."]).success();
497
498 // Try fetching from the remote named 'git'.
499 let output = work_dir.run_jj(["git", "fetch", "--remote=git"]);
500 insta::allow_duplicates! {
501 insta::assert_snapshot!(output, @r"
502 ------- stderr -------
503 Error: Git remote named 'git' is reserved for local Git repository
504 Hint: Run `jj git remote rename` to give a different name.
505 [EOF]
506 [exit status: 1]
507 ");
508 }
509
510 // Fetch remote refs by using the git CLI.
511 git::fetch(work_dir.root(), "git");
512
513 // Implicit import shouldn't fail because of the remote ref.
514 let output = work_dir.run_jj(["bookmark", "list", "--all-remotes"]);
515 insta::allow_duplicates! {
516 insta::assert_snapshot!(output, @r"
517 ------- stderr -------
518 Warning: Failed to import some Git refs:
519 refs/remotes/git/git
520 Hint: Git remote named 'git' is reserved for local Git repository.
521 Use `jj git remote rename` to give a different name.
522 [EOF]
523 ");
524 }
525
526 // Explicit import also works. Warnings are printed twice because this is a
527 // colocated repo. That should be fine since "jj git import" wouldn't be
528 // used in colocated environment.
529 insta::allow_duplicates! {
530 insta::assert_snapshot!(work_dir.run_jj(["git", "import"]), @r"
531 ------- stderr -------
532 Warning: Failed to import some Git refs:
533 refs/remotes/git/git
534 Hint: Git remote named 'git' is reserved for local Git repository.
535 Use `jj git remote rename` to give a different name.
536 Warning: Failed to import some Git refs:
537 refs/remotes/git/git
538 Hint: Git remote named 'git' is reserved for local Git repository.
539 Use `jj git remote rename` to give a different name.
540 Nothing changed.
541 [EOF]
542 ");
543 }
544
545 // The remote can be renamed, and the ref can be imported.
546 work_dir
547 .run_jj(["git", "remote", "rename", "git", "bar"])
548 .success();
549 let output = work_dir.run_jj(["bookmark", "list", "--all-remotes"]);
550 insta::allow_duplicates! {
551 insta::assert_snapshot!(output, @r"
552 git: vkponlun 400c483d message
553 @bar: vkponlun 400c483d message
554 @git: vkponlun 400c483d message
555 [EOF]
556 ------- stderr -------
557 Done importing changes from the underlying Git repo.
558 [EOF]
559 ");
560 }
561}
562
563#[cfg_attr(feature = "git2", test_case(false; "use git2 for remote calls"))]
564#[test_case(true; "spawn a git subprocess for remote calls")]
565fn test_git_fetch_from_remote_with_slashes(subprocess: bool) {
566 let test_env = TestEnvironment::default();
567 if !subprocess {
568 test_env.add_config("git.subprocess = false");
569 }
570 test_env.add_config("git.auto-local-bookmark = true");
571 let work_dir = test_env.work_dir("repo");
572 init_git_remote(&test_env, "source");
573
574 git::init(work_dir.root());
575 git::add_remote(work_dir.root(), "slash/origin", "../source");
576
577 // Existing remote with slash shouldn't block the repo initialization.
578 work_dir.run_jj(["git", "init", "--git-repo=."]).success();
579
580 // Try fetching from the remote named 'git'.
581 let output = work_dir.run_jj(["git", "fetch", "--remote=slash/origin"]);
582 insta::allow_duplicates! {
583 insta::assert_snapshot!(output, @r"
584 ------- stderr -------
585 Error: Git remotes with slashes are incompatible with jj: slash/origin
586 Hint: Run `jj git remote rename` to give a different name.
587 [EOF]
588 [exit status: 1]
589 ");
590 }
591}
592
593#[cfg_attr(feature = "git2", test_case(false; "use git2 for remote calls"))]
594#[test_case(true; "spawn a git subprocess for remote calls")]
595fn test_git_fetch_prune_before_updating_tips(subprocess: bool) {
596 let test_env = TestEnvironment::default();
597 if !subprocess {
598 test_env.add_config("git.subprocess = false");
599 }
600 test_env.add_config("git.auto-local-bookmark = true");
601 test_env.run_jj_in(".", ["git", "init", "repo"]).success();
602 let work_dir = test_env.work_dir("repo");
603 let git_repo = add_git_remote(&test_env, &work_dir, "origin");
604 work_dir.run_jj(["git", "fetch"]).success();
605 insta::allow_duplicates! {
606 insta::assert_snapshot!(get_bookmark_output(&work_dir), @r"
607 origin: qmyrypzk ab8b299e message
608 @origin: qmyrypzk ab8b299e message
609 [EOF]
610 ");
611 }
612
613 // Remove origin bookmark in git repo and create origin/subname
614 let mut origin_reference = git_repo.find_reference("refs/heads/origin").unwrap();
615 let commit_id = origin_reference.peel_to_commit().unwrap().id().detach();
616 origin_reference.delete().unwrap();
617 git_repo
618 .reference(
619 "refs/heads/origin/subname",
620 commit_id,
621 gix::refs::transaction::PreviousValue::MustNotExist,
622 "create new reference",
623 )
624 .unwrap();
625
626 work_dir.run_jj(["git", "fetch"]).success();
627 insta::allow_duplicates! {
628 insta::assert_snapshot!(get_bookmark_output(&work_dir), @r"
629 origin/subname: qmyrypzk ab8b299e message
630 @origin: qmyrypzk ab8b299e message
631 [EOF]
632 ");
633 }
634}
635
636#[cfg_attr(feature = "git2", test_case(false; "use git2 for remote calls"))]
637#[test_case(true; "spawn a git subprocess for remote calls")]
638fn test_git_fetch_conflicting_bookmarks(subprocess: bool) {
639 let test_env = TestEnvironment::default();
640 if !subprocess {
641 test_env.add_config("git.subprocess = false");
642 }
643 test_env.add_config("git.auto-local-bookmark = true");
644 test_env.run_jj_in(".", ["git", "init", "repo"]).success();
645 let work_dir = test_env.work_dir("repo");
646 add_git_remote(&test_env, &work_dir, "rem1");
647
648 // Create a rem1 bookmark locally
649 work_dir.run_jj(["new", "root()"]).success();
650 work_dir
651 .run_jj(["bookmark", "create", "-r@", "rem1"])
652 .success();
653 insta::allow_duplicates! {
654 insta::assert_snapshot!(get_bookmark_output(&work_dir), @r"
655 rem1: kkmpptxz fcdbbd73 (empty) (no description set)
656 [EOF]
657 ");
658 }
659
660 work_dir
661 .run_jj(["git", "fetch", "--remote", "rem1", "--branch", "glob:*"])
662 .success();
663 // This should result in a CONFLICTED bookmark
664 insta::allow_duplicates! {
665 insta::assert_snapshot!(get_bookmark_output(&work_dir), @r"
666 rem1 (conflicted):
667 + kkmpptxz fcdbbd73 (empty) (no description set)
668 + ppspxspk 4acd0343 message
669 @rem1 (behind by 1 commits): ppspxspk 4acd0343 message
670 [EOF]
671 ");
672 }
673}
674
675#[cfg_attr(feature = "git2", test_case(false; "use git2 for remote calls"))]
676#[test_case(true; "spawn a git subprocess for remote calls")]
677fn test_git_fetch_conflicting_bookmarks_colocated(subprocess: bool) {
678 let test_env = TestEnvironment::default();
679 if !subprocess {
680 test_env.add_config("git.subprocess = false");
681 }
682 test_env.add_config("git.auto-local-bookmark = true");
683 let work_dir = test_env.work_dir("repo");
684 git::init(work_dir.root());
685 // create_colocated_repo_and_bookmarks_from_trunk1(&test_env, &repo_path);
686 work_dir
687 .run_jj(["git", "init", "--git-repo", "."])
688 .success();
689 add_git_remote(&test_env, &work_dir, "rem1");
690 insta::allow_duplicates! {
691 insta::assert_snapshot!(get_bookmark_output(&work_dir), @"");
692 }
693
694 // Create a rem1 bookmark locally
695 work_dir.run_jj(["new", "root()"]).success();
696 work_dir
697 .run_jj(["bookmark", "create", "-r@", "rem1"])
698 .success();
699 insta::allow_duplicates! {
700 insta::assert_snapshot!(get_bookmark_output(&work_dir), @r"
701 rem1: zsuskuln f652c321 (empty) (no description set)
702 @git: zsuskuln f652c321 (empty) (no description set)
703 [EOF]
704 ");
705 }
706
707 work_dir
708 .run_jj(["git", "fetch", "--remote", "rem1", "--branch", "rem1"])
709 .success();
710 // This should result in a CONFLICTED bookmark
711 // See https://github.com/jj-vcs/jj/pull/1146#discussion_r1112372340 for the bug this tests for.
712 insta::allow_duplicates! {
713 insta::assert_snapshot!(get_bookmark_output(&work_dir), @r"
714 rem1 (conflicted):
715 + zsuskuln f652c321 (empty) (no description set)
716 + ppspxspk 4acd0343 message
717 @git (behind by 1 commits): zsuskuln f652c321 (empty) (no description set)
718 @rem1 (behind by 1 commits): ppspxspk 4acd0343 message
719 [EOF]
720 ");
721 }
722}
723
724// Helper functions to test obtaining multiple bookmarks at once and changed
725// bookmarks
726fn create_colocated_repo_and_bookmarks_from_trunk1(work_dir: &TestWorkDir) -> String {
727 // Create a colocated repo in `source` to populate it more easily
728 work_dir
729 .run_jj(["git", "init", "--git-repo", "."])
730 .success();
731 create_commit(work_dir, "trunk1", &[]);
732 create_commit(work_dir, "a1", &["trunk1"]);
733 create_commit(work_dir, "a2", &["trunk1"]);
734 create_commit(work_dir, "b", &["trunk1"]);
735 format!(
736 " ===== Source git repo contents =====\n{}",
737 get_log_output(work_dir)
738 )
739}
740
741fn create_trunk2_and_rebase_bookmarks(work_dir: &TestWorkDir) -> String {
742 create_commit(work_dir, "trunk2", &["trunk1"]);
743 for br in ["a1", "a2", "b"] {
744 work_dir
745 .run_jj(["rebase", "-b", br, "-d", "trunk2"])
746 .success();
747 }
748 format!(
749 " ===== Source git repo contents =====\n{}",
750 get_log_output(work_dir)
751 )
752}
753
754#[cfg_attr(feature = "git2", test_case(false; "use git2 for remote calls"))]
755#[test_case(true; "spawn a git subprocess for remote calls")]
756fn test_git_fetch_all(subprocess: bool) {
757 let test_env = TestEnvironment::default();
758 if !subprocess {
759 test_env.add_config("git.subprocess = false");
760 }
761 test_env.add_config("git.auto-local-bookmark = true");
762 test_env.add_config(r#"revset-aliases."immutable_heads()" = "none()""#);
763 let source_dir = test_env.work_dir("source");
764 git::init(source_dir.root());
765
766 // Clone an empty repo. The target repo is a normal `jj` repo, *not* colocated
767 let output = test_env.run_jj_in(".", ["git", "clone", "source", "target"]);
768 insta::allow_duplicates! {
769 insta::assert_snapshot!(output, @r#"
770 ------- stderr -------
771 Fetching into new repo in "$TEST_ENV/target"
772 Nothing changed.
773 [EOF]
774 "#);
775 }
776 let target_dir = test_env.work_dir("target");
777
778 let source_log = create_colocated_repo_and_bookmarks_from_trunk1(&source_dir);
779 insta::allow_duplicates! {
780 insta::assert_snapshot!(source_log, @r#"
781 ===== Source git repo contents =====
782 @ 8cc4df9dd488 "b" b
783 │ ○ e2a95b19b745 "a2" a2
784 ├─╯
785 │ ○ dd42071fe1ad "a1" a1
786 ├─╯
787 ○ 9929b494c411 "trunk1" trunk1
788 ◆ 000000000000 ""
789 [EOF]
790 "#);
791 }
792
793 // Nothing in our repo before the fetch
794 insta::allow_duplicates! {
795 insta::assert_snapshot!(get_log_output(&target_dir), @r#"
796 @ 230dd059e1b0 ""
797 ◆ 000000000000 ""
798 [EOF]
799 "#);
800 }
801 insta::allow_duplicates! {
802 insta::assert_snapshot!(get_bookmark_output(&target_dir), @"");
803 }
804 let output = target_dir.run_jj(["git", "fetch"]);
805 insta::allow_duplicates! {
806 insta::assert_snapshot!(output, @r"
807 ------- stderr -------
808 bookmark: a1@origin [new] tracked
809 bookmark: a2@origin [new] tracked
810 bookmark: b@origin [new] tracked
811 bookmark: trunk1@origin [new] tracked
812 [EOF]
813 ");
814 }
815 insta::allow_duplicates! {
816 insta::assert_snapshot!(get_bookmark_output(&target_dir), @r"
817 a1: spvnozwy dd42071f a1
818 @origin: spvnozwy dd42071f a1
819 a2: qnxtrkvv e2a95b19 a2
820 @origin: qnxtrkvv e2a95b19 a2
821 b: lnxrmsmo 8cc4df9d b
822 @origin: lnxrmsmo 8cc4df9d b
823 trunk1: qzywppkx 9929b494 trunk1
824 @origin: qzywppkx 9929b494 trunk1
825 [EOF]
826 ");
827 }
828 insta::allow_duplicates! {
829 insta::assert_snapshot!(get_log_output(&target_dir), @r#"
830 @ 230dd059e1b0 ""
831 │ ○ 8cc4df9dd488 "b" b
832 │ │ ○ e2a95b19b745 "a2" a2
833 │ ├─╯
834 │ │ ○ dd42071fe1ad "a1" a1
835 │ ├─╯
836 │ ○ 9929b494c411 "trunk1" trunk1
837 ├─╯
838 ◆ 000000000000 ""
839 [EOF]
840 "#);
841 }
842
843 // ==== Change both repos ====
844 // First, change the target repo:
845 let source_log = create_trunk2_and_rebase_bookmarks(&source_dir);
846 insta::allow_duplicates! {
847 insta::assert_snapshot!(source_log, @r#"
848 ===== Source git repo contents =====
849 ○ 7c277a6aa3c3 "b" b
850 │ ○ 698fed8731d8 "a2" a2
851 ├─╯
852 │ ○ a3f2410627ff "a1" a1
853 ├─╯
854 @ e7525a4649e3 "trunk2" trunk2
855 ○ 9929b494c411 "trunk1" trunk1
856 ◆ 000000000000 ""
857 [EOF]
858 "#);
859 }
860 // Change a bookmark in the source repo as well, so that it becomes conflicted.
861 target_dir
862 .run_jj(["describe", "b", "-m=new_descr_for_b_to_create_conflict"])
863 .success();
864
865 // Our repo before and after fetch
866 insta::allow_duplicates! {
867 insta::assert_snapshot!(get_log_output(&target_dir), @r#"
868 @ 230dd059e1b0 ""
869 │ ○ 5b3bc9c99bb3 "new_descr_for_b_to_create_conflict" b*
870 │ │ ○ e2a95b19b745 "a2" a2
871 │ ├─╯
872 │ │ ○ dd42071fe1ad "a1" a1
873 │ ├─╯
874 │ ○ 9929b494c411 "trunk1" trunk1
875 ├─╯
876 ◆ 000000000000 ""
877 [EOF]
878 "#);
879 }
880 insta::allow_duplicates! {
881 insta::assert_snapshot!(get_bookmark_output(&target_dir), @r"
882 a1: spvnozwy dd42071f a1
883 @origin: spvnozwy dd42071f a1
884 a2: qnxtrkvv e2a95b19 a2
885 @origin: qnxtrkvv e2a95b19 a2
886 b: lnxrmsmo 5b3bc9c9 new_descr_for_b_to_create_conflict
887 @origin (ahead by 1 commits, behind by 1 commits): lnxrmsmo hidden 8cc4df9d b
888 trunk1: qzywppkx 9929b494 trunk1
889 @origin: qzywppkx 9929b494 trunk1
890 [EOF]
891 ");
892 }
893 let output = target_dir.run_jj(["git", "fetch"]);
894 insta::allow_duplicates! {
895 insta::assert_snapshot!(output, @r"
896 ------- stderr -------
897 bookmark: a1@origin [updated] tracked
898 bookmark: a2@origin [updated] tracked
899 bookmark: b@origin [updated] tracked
900 bookmark: trunk2@origin [new] tracked
901 Abandoned 2 commits that are no longer reachable.
902 [EOF]
903 ");
904 }
905 insta::allow_duplicates! {
906 insta::assert_snapshot!(get_bookmark_output(&target_dir), @r"
907 a1: vrnsrlyk a3f24106 a1
908 @origin: vrnsrlyk a3f24106 a1
909 a2: vlowznwy 698fed87 a2
910 @origin: vlowznwy 698fed87 a2
911 b (conflicted):
912 - lnxrmsmo hidden 8cc4df9d b
913 + lnxrmsmo 5b3bc9c9 new_descr_for_b_to_create_conflict
914 + uulqyxll 7c277a6a b
915 @origin (behind by 1 commits): uulqyxll 7c277a6a b
916 trunk1: qzywppkx 9929b494 trunk1
917 @origin: qzywppkx 9929b494 trunk1
918 trunk2: lzqpwqnx e7525a46 trunk2
919 @origin: lzqpwqnx e7525a46 trunk2
920 [EOF]
921 ");
922 }
923 insta::allow_duplicates! {
924 insta::assert_snapshot!(get_log_output(&target_dir), @r#"
925 @ 230dd059e1b0 ""
926 │ ○ 7c277a6aa3c3 "b" b?? b@origin
927 │ │ ○ 698fed8731d8 "a2" a2
928 │ ├─╯
929 │ │ ○ a3f2410627ff "a1" a1
930 │ ├─╯
931 │ ○ e7525a4649e3 "trunk2" trunk2
932 │ │ ○ 5b3bc9c99bb3 "new_descr_for_b_to_create_conflict" b??
933 │ ├─╯
934 │ ○ 9929b494c411 "trunk1" trunk1
935 ├─╯
936 ◆ 000000000000 ""
937 [EOF]
938 "#);
939 }
940}
941
942#[cfg_attr(feature = "git2", test_case(false; "use git2 for remote calls"))]
943#[test_case(true; "spawn a git subprocess for remote calls")]
944fn test_git_fetch_some_of_many_bookmarks(subprocess: bool) {
945 let test_env = TestEnvironment::default();
946 if !subprocess {
947 test_env.add_config("git.subprocess = false");
948 }
949 test_env.add_config("git.auto-local-bookmark = true");
950 test_env.add_config(r#"revset-aliases."immutable_heads()" = "none()""#);
951 let source_dir = test_env.work_dir("source");
952 git::init(source_dir.root());
953
954 // Clone an empty repo. The target repo is a normal `jj` repo, *not* colocated
955 let output = test_env.run_jj_in(".", ["git", "clone", "source", "target"]);
956 insta::allow_duplicates! {
957 insta::assert_snapshot!(output, @r#"
958 ------- stderr -------
959 Fetching into new repo in "$TEST_ENV/target"
960 Nothing changed.
961 [EOF]
962 "#);
963 }
964 let target_dir = test_env.work_dir("target");
965
966 let source_log = create_colocated_repo_and_bookmarks_from_trunk1(&source_dir);
967 insta::allow_duplicates! {
968 insta::assert_snapshot!(source_log, @r#"
969 ===== Source git repo contents =====
970 @ 8cc4df9dd488 "b" b
971 │ ○ e2a95b19b745 "a2" a2
972 ├─╯
973 │ ○ dd42071fe1ad "a1" a1
974 ├─╯
975 ○ 9929b494c411 "trunk1" trunk1
976 ◆ 000000000000 ""
977 [EOF]
978 "#);
979 }
980
981 // Test an error message
982 let output = target_dir.run_jj(["git", "fetch", "--branch", "glob:^:a*"]);
983 insta::allow_duplicates! {
984 insta::assert_snapshot!(output, @r"
985 ------- stderr -------
986 Error: Invalid branch pattern provided. When fetching, branch names and globs may not contain the characters `:`, `^`, `?`, `[`, `]`
987 [EOF]
988 [exit status: 1]
989 ");
990 }
991 let output = target_dir.run_jj(["git", "fetch", "--branch", "a*"]);
992 insta::allow_duplicates! {
993 insta::assert_snapshot!(output, @r"
994 ------- stderr -------
995 Error: Branch names may not include `*`.
996 Hint: Prefix the pattern with `glob:` to expand `*` as a glob
997 [EOF]
998 [exit status: 1]
999 ");
1000 }
1001
1002 // Nothing in our repo before the fetch
1003 insta::allow_duplicates! {
1004 insta::assert_snapshot!(get_log_output(&target_dir), @r#"
1005 @ 230dd059e1b0 ""
1006 ◆ 000000000000 ""
1007 [EOF]
1008 "#);
1009 }
1010 // Fetch one bookmark...
1011 let output = target_dir.run_jj(["git", "fetch", "--branch", "b"]);
1012 insta::allow_duplicates! {
1013 insta::assert_snapshot!(output, @r"
1014 ------- stderr -------
1015 bookmark: b@origin [new] tracked
1016 [EOF]
1017 ");
1018 }
1019 insta::allow_duplicates! {
1020 insta::assert_snapshot!(get_log_output(&target_dir), @r#"
1021 @ 230dd059e1b0 ""
1022 │ ○ 8cc4df9dd488 "b" b
1023 │ ○ 9929b494c411 "trunk1"
1024 ├─╯
1025 ◆ 000000000000 ""
1026 [EOF]
1027 "#);
1028 }
1029 // ...check what the intermediate state looks like...
1030 insta::allow_duplicates! {
1031 insta::assert_snapshot!(get_bookmark_output(&target_dir), @r"
1032 b: lnxrmsmo 8cc4df9d b
1033 @origin: lnxrmsmo 8cc4df9d b
1034 [EOF]
1035 ");
1036 }
1037 // ...then fetch two others with a glob.
1038 let output = target_dir.run_jj(["git", "fetch", "--branch", "glob:a*"]);
1039 insta::allow_duplicates! {
1040 insta::assert_snapshot!(output, @r"
1041 ------- stderr -------
1042 bookmark: a1@origin [new] tracked
1043 bookmark: a2@origin [new] tracked
1044 [EOF]
1045 ");
1046 }
1047 insta::allow_duplicates! {
1048 insta::assert_snapshot!(get_log_output(&target_dir), @r#"
1049 @ 230dd059e1b0 ""
1050 │ ○ e2a95b19b745 "a2" a2
1051 │ │ ○ dd42071fe1ad "a1" a1
1052 │ ├─╯
1053 │ │ ○ 8cc4df9dd488 "b" b
1054 │ ├─╯
1055 │ ○ 9929b494c411 "trunk1"
1056 ├─╯
1057 ◆ 000000000000 ""
1058 [EOF]
1059 "#);
1060 }
1061 // Fetching the same bookmark again
1062 let output = target_dir.run_jj(["git", "fetch", "--branch", "a1"]);
1063 insta::allow_duplicates! {
1064 insta::assert_snapshot!(output, @r"
1065 ------- stderr -------
1066 Nothing changed.
1067 [EOF]
1068 ");
1069 }
1070 insta::allow_duplicates! {
1071 insta::assert_snapshot!(get_log_output(&target_dir), @r#"
1072 @ 230dd059e1b0 ""
1073 │ ○ e2a95b19b745 "a2" a2
1074 │ │ ○ dd42071fe1ad "a1" a1
1075 │ ├─╯
1076 │ │ ○ 8cc4df9dd488 "b" b
1077 │ ├─╯
1078 │ ○ 9929b494c411 "trunk1"
1079 ├─╯
1080 ◆ 000000000000 ""
1081 [EOF]
1082 "#);
1083 }
1084
1085 // ==== Change both repos ====
1086 // First, change the target repo:
1087 let source_log = create_trunk2_and_rebase_bookmarks(&source_dir);
1088 insta::allow_duplicates! {
1089 insta::assert_snapshot!(source_log, @r#"
1090 ===== Source git repo contents =====
1091 ○ 96c8ad25ad36 "b" b
1092 │ ○ 9ccd9f75fc0c "a2" a2
1093 ├─╯
1094 │ ○ 527d2e46a87f "a1" a1
1095 ├─╯
1096 @ 2f34a0e70741 "trunk2" trunk2
1097 ○ 9929b494c411 "trunk1" trunk1
1098 ◆ 000000000000 ""
1099 [EOF]
1100 "#);
1101 }
1102 // Change a bookmark in the source repo as well, so that it becomes conflicted.
1103 target_dir
1104 .run_jj(["describe", "b", "-m=new_descr_for_b_to_create_conflict"])
1105 .success();
1106
1107 // Our repo before and after fetch of two bookmarks
1108 insta::allow_duplicates! {
1109 insta::assert_snapshot!(get_log_output(&target_dir), @r#"
1110 @ 230dd059e1b0 ""
1111 │ ○ e9445259b932 "new_descr_for_b_to_create_conflict" b*
1112 │ │ ○ e2a95b19b745 "a2" a2
1113 │ ├─╯
1114 │ │ ○ dd42071fe1ad "a1" a1
1115 │ ├─╯
1116 │ ○ 9929b494c411 "trunk1"
1117 ├─╯
1118 ◆ 000000000000 ""
1119 [EOF]
1120 "#);
1121 }
1122 let output = target_dir.run_jj(["git", "fetch", "--branch", "b", "--branch", "a1"]);
1123 insta::allow_duplicates! {
1124 insta::assert_snapshot!(output, @r"
1125 ------- stderr -------
1126 bookmark: a1@origin [updated] tracked
1127 bookmark: b@origin [updated] tracked
1128 Abandoned 1 commits that are no longer reachable.
1129 [EOF]
1130 ");
1131 }
1132 insta::allow_duplicates! {
1133 insta::assert_snapshot!(get_log_output(&target_dir), @r#"
1134 @ 230dd059e1b0 ""
1135 │ ○ 96c8ad25ad36 "b" b?? b@origin
1136 │ │ ○ 527d2e46a87f "a1" a1
1137 │ ├─╯
1138 │ ○ 2f34a0e70741 "trunk2"
1139 │ │ ○ e9445259b932 "new_descr_for_b_to_create_conflict" b??
1140 │ ├─╯
1141 │ │ ○ e2a95b19b745 "a2" a2
1142 │ ├─╯
1143 │ ○ 9929b494c411 "trunk1"
1144 ├─╯
1145 ◆ 000000000000 ""
1146 [EOF]
1147 "#);
1148 }
1149
1150 // We left a2 where it was before, let's see how `jj bookmark list` sees this.
1151 insta::allow_duplicates! {
1152 insta::assert_snapshot!(get_bookmark_output(&target_dir), @r"
1153 a1: qptloxlm 527d2e46 a1
1154 @origin: qptloxlm 527d2e46 a1
1155 a2: qnxtrkvv e2a95b19 a2
1156 @origin: qnxtrkvv e2a95b19 a2
1157 b (conflicted):
1158 - lnxrmsmo hidden 8cc4df9d b
1159 + lnxrmsmo e9445259 new_descr_for_b_to_create_conflict
1160 + rruvkzpm 96c8ad25 b
1161 @origin (behind by 1 commits): rruvkzpm 96c8ad25 b
1162 [EOF]
1163 ");
1164 }
1165 // Now, let's fetch a2 and double-check that fetching a1 and b again doesn't do
1166 // anything.
1167 let output = target_dir.run_jj(["git", "fetch", "--branch", "b", "--branch", "glob:a*"]);
1168 insta::allow_duplicates! {
1169 insta::assert_snapshot!(output, @r"
1170 ------- stderr -------
1171 bookmark: a2@origin [updated] tracked
1172 Abandoned 1 commits that are no longer reachable.
1173 [EOF]
1174 ");
1175 }
1176 insta::allow_duplicates! {
1177 insta::assert_snapshot!(get_log_output(&target_dir), @r#"
1178 @ 230dd059e1b0 ""
1179 │ ○ 9ccd9f75fc0c "a2" a2
1180 │ │ ○ 96c8ad25ad36 "b" b?? b@origin
1181 │ ├─╯
1182 │ │ ○ 527d2e46a87f "a1" a1
1183 │ ├─╯
1184 │ ○ 2f34a0e70741 "trunk2"
1185 │ │ ○ e9445259b932 "new_descr_for_b_to_create_conflict" b??
1186 │ ├─╯
1187 │ ○ 9929b494c411 "trunk1"
1188 ├─╯
1189 ◆ 000000000000 ""
1190 [EOF]
1191 "#);
1192 }
1193 insta::allow_duplicates! {
1194 insta::assert_snapshot!(get_bookmark_output(&target_dir), @r"
1195 a1: qptloxlm 527d2e46 a1
1196 @origin: qptloxlm 527d2e46 a1
1197 a2: ltuqxttq 9ccd9f75 a2
1198 @origin: ltuqxttq 9ccd9f75 a2
1199 b (conflicted):
1200 - lnxrmsmo hidden 8cc4df9d b
1201 + lnxrmsmo e9445259 new_descr_for_b_to_create_conflict
1202 + rruvkzpm 96c8ad25 b
1203 @origin (behind by 1 commits): rruvkzpm 96c8ad25 b
1204 [EOF]
1205 ");
1206 }
1207}
1208
1209#[cfg_attr(feature = "git2", test_case(false; "use git2 for remote calls"))]
1210#[test_case(true; "spawn a git subprocess for remote calls")]
1211fn test_git_fetch_bookmarks_some_missing(subprocess: bool) {
1212 let test_env = TestEnvironment::default();
1213 if !subprocess {
1214 test_env.add_config("git.subprocess = false");
1215 }
1216 test_env.add_config("git.auto-local-bookmark = true");
1217 test_env.run_jj_in(".", ["git", "init", "repo"]).success();
1218 let work_dir = test_env.work_dir("repo");
1219 add_git_remote(&test_env, &work_dir, "origin");
1220 add_git_remote(&test_env, &work_dir, "rem1");
1221 add_git_remote(&test_env, &work_dir, "rem2");
1222 add_git_remote(&test_env, &work_dir, "rem3");
1223
1224 // single missing bookmark, implicit remotes (@origin)
1225 let output = work_dir.run_jj(["git", "fetch", "--branch", "noexist"]);
1226 insta::allow_duplicates! {
1227 insta::assert_snapshot!(output, @r"
1228 ------- stderr -------
1229 Warning: No branch matching `noexist` found on any specified/configured remote
1230 Nothing changed.
1231 [EOF]
1232 ");
1233 }
1234 insta::allow_duplicates! {
1235 insta::assert_snapshot!(get_bookmark_output(&work_dir), @"");
1236 }
1237
1238 // multiple missing bookmarks, implicit remotes (@origin)
1239 let output = work_dir.run_jj([
1240 "git", "fetch", "--branch", "noexist1", "--branch", "noexist2",
1241 ]);
1242 insta::allow_duplicates! {
1243 insta::assert_snapshot!(output, @r"
1244 ------- stderr -------
1245 Warning: No branch matching `noexist1` found on any specified/configured remote
1246 Warning: No branch matching `noexist2` found on any specified/configured remote
1247 Nothing changed.
1248 [EOF]
1249 ");
1250 }
1251 insta::allow_duplicates! {
1252 insta::assert_snapshot!(get_bookmark_output(&work_dir), @"");
1253 }
1254
1255 // single existing bookmark, implicit remotes (@origin)
1256 let output = work_dir.run_jj(["git", "fetch", "--branch", "origin"]);
1257 insta::allow_duplicates! {
1258 insta::assert_snapshot!(output, @r"
1259 ------- stderr -------
1260 bookmark: origin@origin [new] tracked
1261 [EOF]
1262 ");
1263 }
1264 insta::allow_duplicates! {
1265 insta::assert_snapshot!(get_bookmark_output(&work_dir), @r"
1266 origin: qmyrypzk ab8b299e message
1267 @origin: qmyrypzk ab8b299e message
1268 [EOF]
1269 ");
1270 }
1271
1272 // multiple existing bookmark, explicit remotes, each bookmark is only in one
1273 // remote.
1274 let output = work_dir.run_jj([
1275 "git", "fetch", "--branch", "rem1", "--branch", "rem2", "--branch", "rem3", "--remote",
1276 "rem1", "--remote", "rem2", "--remote", "rem3",
1277 ]);
1278 insta::allow_duplicates! {
1279 insta::assert_snapshot!(output, @r"
1280 ------- stderr -------
1281 bookmark: rem1@rem1 [new] tracked
1282 bookmark: rem2@rem2 [new] tracked
1283 bookmark: rem3@rem3 [new] tracked
1284 [EOF]
1285 ");
1286 }
1287 insta::allow_duplicates! {
1288 insta::assert_snapshot!(get_bookmark_output(&work_dir), @r"
1289 origin: qmyrypzk ab8b299e message
1290 @origin: qmyrypzk ab8b299e message
1291 rem1: ppspxspk 4acd0343 message
1292 @rem1: ppspxspk 4acd0343 message
1293 rem2: pzqqpnpo 44c57802 message
1294 @rem2: pzqqpnpo 44c57802 message
1295 rem3: wrzwlmys 45a3faef message
1296 @rem3: wrzwlmys 45a3faef message
1297 [EOF]
1298 ")
1299 }
1300
1301 // multiple bookmarks, one exists, one doesn't
1302 let output = work_dir.run_jj([
1303 "git", "fetch", "--branch", "rem1", "--branch", "notexist", "--remote", "rem1",
1304 ]);
1305 insta::allow_duplicates! {
1306 insta::assert_snapshot!(output, @r"
1307 ------- stderr -------
1308 Warning: No branch matching `notexist` found on any specified/configured remote
1309 Nothing changed.
1310 [EOF]
1311 ");
1312 }
1313 insta::allow_duplicates! {
1314 insta::assert_snapshot!(get_bookmark_output(&work_dir), @r"
1315 origin: qmyrypzk ab8b299e message
1316 @origin: qmyrypzk ab8b299e message
1317 rem1: ppspxspk 4acd0343 message
1318 @rem1: ppspxspk 4acd0343 message
1319 rem2: pzqqpnpo 44c57802 message
1320 @rem2: pzqqpnpo 44c57802 message
1321 rem3: wrzwlmys 45a3faef message
1322 @rem3: wrzwlmys 45a3faef message
1323 [EOF]
1324 ");
1325 }
1326}
1327
1328#[test]
1329fn test_git_fetch_bookmarks_missing_with_subprocess_localized_message() {
1330 let test_env = TestEnvironment::default();
1331 test_env.run_jj_in(".", ["git", "init", "repo"]).success();
1332 let work_dir = test_env.work_dir("repo");
1333 add_git_remote(&test_env, &work_dir, "origin");
1334
1335 // "fatal: couldn't find remote ref %s" shouldn't be localized.
1336 let output = work_dir.run_jj_with(|cmd| {
1337 cmd.args(["git", "fetch", "--branch=unknown"])
1338 // Initialize locale as "en_US" which is the most common.
1339 .env("LC_ALL", "en_US.UTF-8")
1340 // Set some other locale variables for testing.
1341 .env("LC_MESSAGES", "en_US.UTF-8")
1342 .env("LANG", "en_US.UTF-8")
1343 // GNU gettext prioritizes LANGUAGE if translation is enabled. It works
1344 // no matter if system locale exists or not.
1345 .env("LANGUAGE", "zh_TW")
1346 });
1347 insta::assert_snapshot!(output, @r"
1348 ------- stderr -------
1349 Warning: No branch matching `unknown` found on any specified/configured remote
1350 Nothing changed.
1351 [EOF]
1352 ");
1353}
1354
1355// See `test_undo_restore_commands.rs` for fetch-undo-push and fetch-undo-fetch
1356// of the same bookmarks for various kinds of undo.
1357#[cfg_attr(feature = "git2", test_case(false; "use git2 for remote calls"))]
1358#[test_case(true; "spawn a git subprocess for remote calls")]
1359fn test_git_fetch_undo(subprocess: bool) {
1360 let test_env = TestEnvironment::default();
1361 if !subprocess {
1362 test_env.add_config("git.subprocess = false");
1363 }
1364 test_env.add_config("git.auto-local-bookmark = true");
1365 let source_dir = test_env.work_dir("source");
1366 git::init(source_dir.root());
1367
1368 // Clone an empty repo. The target repo is a normal `jj` repo, *not* colocated
1369 let output = test_env.run_jj_in(".", ["git", "clone", "source", "target"]);
1370 insta::allow_duplicates! {
1371 insta::assert_snapshot!(output, @r#"
1372 ------- stderr -------
1373 Fetching into new repo in "$TEST_ENV/target"
1374 Nothing changed.
1375 [EOF]
1376 "#);
1377 }
1378 let target_dir = test_env.work_dir("target");
1379
1380 let source_log = create_colocated_repo_and_bookmarks_from_trunk1(&source_dir);
1381 insta::allow_duplicates! {
1382 insta::assert_snapshot!(source_log, @r#"
1383 ===== Source git repo contents =====
1384 @ 8cc4df9dd488 "b" b
1385 │ ○ e2a95b19b745 "a2" a2
1386 ├─╯
1387 │ ○ dd42071fe1ad "a1" a1
1388 ├─╯
1389 ○ 9929b494c411 "trunk1" trunk1
1390 ◆ 000000000000 ""
1391 [EOF]
1392 "#);
1393 }
1394
1395 // Fetch 2 bookmarks
1396 let output = target_dir.run_jj(["git", "fetch", "--branch", "b", "--branch", "a1"]);
1397 insta::allow_duplicates! {
1398 insta::assert_snapshot!(output, @r"
1399 ------- stderr -------
1400 bookmark: a1@origin [new] tracked
1401 bookmark: b@origin [new] tracked
1402 [EOF]
1403 ");
1404 }
1405 insta::allow_duplicates! {
1406 insta::assert_snapshot!(get_log_output(&target_dir), @r#"
1407 @ 230dd059e1b0 ""
1408 │ ○ 8cc4df9dd488 "b" b
1409 │ │ ○ dd42071fe1ad "a1" a1
1410 │ ├─╯
1411 │ ○ 9929b494c411 "trunk1"
1412 ├─╯
1413 ◆ 000000000000 ""
1414 [EOF]
1415 "#);
1416 }
1417 let output = target_dir.run_jj(["undo"]);
1418 insta::allow_duplicates! {
1419 insta::assert_snapshot!(output, @r"
1420 ------- stderr -------
1421 Undid operation: 4bd67fb242bc (2001-02-03 08:05:18) fetch from git remote(s) origin
1422 [EOF]
1423 ");
1424 }
1425 // The undo works as expected
1426 insta::allow_duplicates! {
1427 insta::assert_snapshot!(get_log_output(&target_dir), @r#"
1428 @ 230dd059e1b0 ""
1429 ◆ 000000000000 ""
1430 [EOF]
1431 "#);
1432 }
1433 // Now try to fetch just one bookmark
1434 let output = target_dir.run_jj(["git", "fetch", "--branch", "b"]);
1435 insta::allow_duplicates! {
1436 insta::assert_snapshot!(output, @r"
1437 ------- stderr -------
1438 bookmark: b@origin [new] tracked
1439 [EOF]
1440 ");
1441 }
1442 insta::allow_duplicates! {
1443 insta::assert_snapshot!(get_log_output(&target_dir), @r#"
1444 @ 230dd059e1b0 ""
1445 │ ○ 8cc4df9dd488 "b" b
1446 │ ○ 9929b494c411 "trunk1"
1447 ├─╯
1448 ◆ 000000000000 ""
1449 [EOF]
1450 "#);
1451 }
1452}
1453
1454// Compare to `test_git_import_undo` in test_git_import_export
1455// TODO: Explain why these behaviors are useful
1456#[cfg_attr(feature = "git2", test_case(false; "use git2 for remote calls"))]
1457#[test_case(true; "spawn a git subprocess for remote calls")]
1458fn test_fetch_undo_what(subprocess: bool) {
1459 let test_env = TestEnvironment::default();
1460 if !subprocess {
1461 test_env.add_config("git.subprocess = false");
1462 }
1463 test_env.add_config("git.auto-local-bookmark = true");
1464 let source_dir = test_env.work_dir("source");
1465 git::init(source_dir.root());
1466
1467 // Clone an empty repo. The target repo is a normal `jj` repo, *not* colocated
1468 let output = test_env.run_jj_in(".", ["git", "clone", "source", "target"]);
1469 insta::allow_duplicates! {
1470 insta::assert_snapshot!(output, @r#"
1471 ------- stderr -------
1472 Fetching into new repo in "$TEST_ENV/target"
1473 Nothing changed.
1474 [EOF]
1475 "#);
1476 }
1477 let work_dir = test_env.work_dir("target");
1478
1479 let source_log = create_colocated_repo_and_bookmarks_from_trunk1(&source_dir);
1480 insta::allow_duplicates! {
1481 insta::assert_snapshot!(source_log, @r#"
1482 ===== Source git repo contents =====
1483 @ 8cc4df9dd488 "b" b
1484 │ ○ e2a95b19b745 "a2" a2
1485 ├─╯
1486 │ ○ dd42071fe1ad "a1" a1
1487 ├─╯
1488 ○ 9929b494c411 "trunk1" trunk1
1489 ◆ 000000000000 ""
1490 [EOF]
1491 "#);
1492 }
1493
1494 // Initial state we will try to return to after `op restore`. There are no
1495 // bookmarks.
1496 insta::allow_duplicates! {
1497 insta::assert_snapshot!(get_bookmark_output(&work_dir), @"");
1498 }
1499 let base_operation_id = work_dir.current_operation_id();
1500
1501 // Fetch a bookmark
1502 let output = work_dir.run_jj(["git", "fetch", "--branch", "b"]);
1503 insta::allow_duplicates! {
1504 insta::assert_snapshot!(output, @r"
1505 ------- stderr -------
1506 bookmark: b@origin [new] tracked
1507 [EOF]
1508 ");
1509 }
1510 insta::allow_duplicates! {
1511 insta::assert_snapshot!(get_log_output(&work_dir), @r#"
1512 @ 230dd059e1b0 ""
1513 │ ○ 8cc4df9dd488 "b" b
1514 │ ○ 9929b494c411 "trunk1"
1515 ├─╯
1516 ◆ 000000000000 ""
1517 [EOF]
1518 "#);
1519 }
1520 insta::allow_duplicates! {
1521 insta::assert_snapshot!(get_bookmark_output(&work_dir), @r"
1522 b: lnxrmsmo 8cc4df9d b
1523 @origin: lnxrmsmo 8cc4df9d b
1524 [EOF]
1525 ");
1526 }
1527
1528 // We can undo the change in the repo without moving the remote-tracking
1529 // bookmark
1530 let output = work_dir.run_jj(["op", "restore", "--what", "repo", &base_operation_id]);
1531 insta::allow_duplicates! {
1532 insta::assert_snapshot!(output, @r"
1533 ------- stderr -------
1534 Restored to operation: eac759b9ab75 (2001-02-03 08:05:07) add workspace 'default'
1535 [EOF]
1536 ");
1537 }
1538 insta::allow_duplicates! {
1539 insta::assert_snapshot!(get_bookmark_output(&work_dir), @r"
1540 b (deleted)
1541 @origin: lnxrmsmo hidden 8cc4df9d b
1542 [EOF]
1543 ");
1544 }
1545
1546 // Now, let's demo restoring just the remote-tracking bookmark. First, let's
1547 // change our local repo state...
1548 work_dir
1549 .run_jj(["bookmark", "c", "-r@", "newbookmark"])
1550 .success();
1551 insta::allow_duplicates! {
1552 insta::assert_snapshot!(get_bookmark_output(&work_dir), @r"
1553 b (deleted)
1554 @origin: lnxrmsmo hidden 8cc4df9d b
1555 newbookmark: qpvuntsm 230dd059 (empty) (no description set)
1556 [EOF]
1557 ");
1558 }
1559 // Restoring just the remote-tracking state will not affect `newbookmark`, but
1560 // will eliminate `b@origin`.
1561 let output = work_dir.run_jj([
1562 "op",
1563 "restore",
1564 "--what",
1565 "remote-tracking",
1566 &base_operation_id,
1567 ]);
1568 insta::allow_duplicates! {
1569 insta::assert_snapshot!(output, @r"
1570 ------- stderr -------
1571 Restored to operation: eac759b9ab75 (2001-02-03 08:05:07) add workspace 'default'
1572 [EOF]
1573 ");
1574 }
1575 insta::allow_duplicates! {
1576 insta::assert_snapshot!(get_bookmark_output(&work_dir), @r"
1577 newbookmark: qpvuntsm 230dd059 (empty) (no description set)
1578 [EOF]
1579 ");
1580 }
1581}
1582
1583#[cfg_attr(feature = "git2", test_case(false; "use git2 for remote calls"))]
1584#[test_case(true; "spawn a git subprocess for remote calls")]
1585fn test_git_fetch_remove_fetch(subprocess: bool) {
1586 let test_env = TestEnvironment::default();
1587 if !subprocess {
1588 test_env.add_config("git.subprocess = false");
1589 }
1590 test_env.add_config("git.auto-local-bookmark = true");
1591 test_env.run_jj_in(".", ["git", "init", "repo"]).success();
1592 let work_dir = test_env.work_dir("repo");
1593 add_git_remote(&test_env, &work_dir, "origin");
1594
1595 work_dir
1596 .run_jj(["bookmark", "create", "-r@", "origin"])
1597 .success();
1598 insta::allow_duplicates! {
1599 insta::assert_snapshot!(get_bookmark_output(&work_dir), @r"
1600 origin: qpvuntsm 230dd059 (empty) (no description set)
1601 [EOF]
1602 ");
1603 }
1604
1605 work_dir.run_jj(["git", "fetch"]).success();
1606 insta::allow_duplicates! {
1607 insta::assert_snapshot!(get_bookmark_output(&work_dir), @r"
1608 origin (conflicted):
1609 + qpvuntsm 230dd059 (empty) (no description set)
1610 + qmyrypzk ab8b299e message
1611 @origin (behind by 1 commits): qmyrypzk ab8b299e message
1612 [EOF]
1613 ");
1614 }
1615
1616 work_dir
1617 .run_jj(["git", "remote", "remove", "origin"])
1618 .success();
1619 insta::allow_duplicates! {
1620 insta::assert_snapshot!(get_bookmark_output(&work_dir), @r"
1621 origin (conflicted):
1622 + qpvuntsm 230dd059 (empty) (no description set)
1623 + qmyrypzk ab8b299e message
1624 [EOF]
1625 ");
1626 }
1627
1628 work_dir
1629 .run_jj(["git", "remote", "add", "origin", "../origin"])
1630 .success();
1631
1632 // Check that origin@origin is properly recreated
1633 let output = work_dir.run_jj(["git", "fetch"]);
1634 insta::allow_duplicates! {
1635 insta::assert_snapshot!(output, @r"
1636 ------- stderr -------
1637 bookmark: origin@origin [new] tracked
1638 [EOF]
1639 ");
1640 }
1641 insta::allow_duplicates! {
1642 insta::assert_snapshot!(get_bookmark_output(&work_dir), @r"
1643 origin (conflicted):
1644 + qpvuntsm 230dd059 (empty) (no description set)
1645 + qmyrypzk ab8b299e message
1646 @origin (behind by 1 commits): qmyrypzk ab8b299e message
1647 [EOF]
1648 ");
1649 }
1650}
1651
1652#[cfg_attr(feature = "git2", test_case(false; "use git2 for remote calls"))]
1653#[test_case(true; "spawn a git subprocess for remote calls")]
1654fn test_git_fetch_rename_fetch(subprocess: bool) {
1655 let test_env = TestEnvironment::default();
1656 if !subprocess {
1657 test_env.add_config("git.subprocess = false");
1658 }
1659 test_env.add_config("git.auto-local-bookmark = true");
1660 test_env.run_jj_in(".", ["git", "init", "repo"]).success();
1661 let work_dir = test_env.work_dir("repo");
1662 add_git_remote(&test_env, &work_dir, "origin");
1663
1664 work_dir
1665 .run_jj(["bookmark", "create", "-r@", "origin"])
1666 .success();
1667 insta::allow_duplicates! {
1668 insta::assert_snapshot!(get_bookmark_output(&work_dir), @r"
1669 origin: qpvuntsm 230dd059 (empty) (no description set)
1670 [EOF]
1671 ");
1672 }
1673
1674 work_dir.run_jj(["git", "fetch"]).success();
1675 insta::allow_duplicates! {
1676 insta::assert_snapshot!(get_bookmark_output(&work_dir), @r"
1677 origin (conflicted):
1678 + qpvuntsm 230dd059 (empty) (no description set)
1679 + qmyrypzk ab8b299e message
1680 @origin (behind by 1 commits): qmyrypzk ab8b299e message
1681 [EOF]
1682 ");
1683 }
1684
1685 work_dir
1686 .run_jj(["git", "remote", "rename", "origin", "upstream"])
1687 .success();
1688 insta::allow_duplicates! {
1689 insta::assert_snapshot!(get_bookmark_output(&work_dir), @r"
1690 origin (conflicted):
1691 + qpvuntsm 230dd059 (empty) (no description set)
1692 + qmyrypzk ab8b299e message
1693 @upstream (behind by 1 commits): qmyrypzk ab8b299e message
1694 [EOF]
1695 ");
1696 }
1697
1698 // Check that jj indicates that nothing has changed
1699 let output = work_dir.run_jj(["git", "fetch", "--remote", "upstream"]);
1700 insta::allow_duplicates! {
1701 insta::assert_snapshot!(output, @r"
1702 ------- stderr -------
1703 Nothing changed.
1704 [EOF]
1705 ");
1706 }
1707}
1708
1709#[cfg_attr(feature = "git2", test_case(false; "use git2 for remote calls"))]
1710#[test_case(true; "spawn a git subprocess for remote calls")]
1711fn test_git_fetch_removed_bookmark(subprocess: bool) {
1712 let test_env = TestEnvironment::default();
1713 if !subprocess {
1714 test_env.add_config("git.subprocess = false");
1715 }
1716 test_env.add_config("git.auto-local-bookmark = true");
1717 let source_dir = test_env.work_dir("source");
1718 git::init(source_dir.root());
1719
1720 // Clone an empty repo. The target repo is a normal `jj` repo, *not* colocated
1721 let output = test_env.run_jj_in(".", ["git", "clone", "source", "target"]);
1722 insta::allow_duplicates! {
1723 insta::assert_snapshot!(output, @r#"
1724 ------- stderr -------
1725 Fetching into new repo in "$TEST_ENV/target"
1726 Nothing changed.
1727 [EOF]
1728 "#);
1729 }
1730 let target_dir = test_env.work_dir("target");
1731
1732 let source_log = create_colocated_repo_and_bookmarks_from_trunk1(&source_dir);
1733 insta::allow_duplicates! {
1734 insta::assert_snapshot!(source_log, @r#"
1735 ===== Source git repo contents =====
1736 @ 8cc4df9dd488 "b" b
1737 │ ○ e2a95b19b745 "a2" a2
1738 ├─╯
1739 │ ○ dd42071fe1ad "a1" a1
1740 ├─╯
1741 ○ 9929b494c411 "trunk1" trunk1
1742 ◆ 000000000000 ""
1743 [EOF]
1744 "#);
1745 }
1746
1747 // Fetch all bookmarks
1748 let output = target_dir.run_jj(["git", "fetch"]);
1749 insta::allow_duplicates! {
1750 insta::assert_snapshot!(output, @r"
1751 ------- stderr -------
1752 bookmark: a1@origin [new] tracked
1753 bookmark: a2@origin [new] tracked
1754 bookmark: b@origin [new] tracked
1755 bookmark: trunk1@origin [new] tracked
1756 [EOF]
1757 ");
1758 }
1759 insta::allow_duplicates! {
1760 insta::assert_snapshot!(get_log_output(&target_dir), @r#"
1761 @ 230dd059e1b0 ""
1762 │ ○ 8cc4df9dd488 "b" b
1763 │ │ ○ e2a95b19b745 "a2" a2
1764 │ ├─╯
1765 │ │ ○ dd42071fe1ad "a1" a1
1766 │ ├─╯
1767 │ ○ 9929b494c411 "trunk1" trunk1
1768 ├─╯
1769 ◆ 000000000000 ""
1770 [EOF]
1771 "#);
1772 }
1773
1774 // Remove a2 bookmark in origin
1775 source_dir
1776 .run_jj(["bookmark", "forget", "--include-remotes", "a2"])
1777 .success();
1778
1779 // Fetch bookmark a1 from origin and check that a2 is still there
1780 let output = target_dir.run_jj(["git", "fetch", "--branch", "a1"]);
1781 insta::allow_duplicates! {
1782 insta::assert_snapshot!(output, @r"
1783 ------- stderr -------
1784 Nothing changed.
1785 [EOF]
1786 ");
1787 }
1788 insta::allow_duplicates! {
1789 insta::assert_snapshot!(get_log_output(&target_dir), @r#"
1790 @ 230dd059e1b0 ""
1791 │ ○ 8cc4df9dd488 "b" b
1792 │ │ ○ e2a95b19b745 "a2" a2
1793 │ ├─╯
1794 │ │ ○ dd42071fe1ad "a1" a1
1795 │ ├─╯
1796 │ ○ 9929b494c411 "trunk1" trunk1
1797 ├─╯
1798 ◆ 000000000000 ""
1799 [EOF]
1800 "#);
1801 }
1802
1803 // Fetch bookmarks a2 from origin, and check that it has been removed locally
1804 let output = target_dir.run_jj(["git", "fetch", "--branch", "a2"]);
1805 insta::allow_duplicates! {
1806 insta::assert_snapshot!(output, @r"
1807 ------- stderr -------
1808 bookmark: a2@origin [deleted] untracked
1809 Abandoned 1 commits that are no longer reachable.
1810 [EOF]
1811 ");
1812 }
1813 insta::allow_duplicates! {
1814 insta::assert_snapshot!(get_log_output(&target_dir), @r#"
1815 @ 230dd059e1b0 ""
1816 │ ○ 8cc4df9dd488 "b" b
1817 │ │ ○ dd42071fe1ad "a1" a1
1818 │ ├─╯
1819 │ ○ 9929b494c411 "trunk1" trunk1
1820 ├─╯
1821 ◆ 000000000000 ""
1822 [EOF]
1823 "#);
1824 }
1825}
1826
1827#[cfg_attr(feature = "git2", test_case(false; "use git2 for remote calls"))]
1828#[test_case(true; "spawn a git subprocess for remote calls")]
1829fn test_git_fetch_removed_parent_bookmark(subprocess: bool) {
1830 let test_env = TestEnvironment::default();
1831 if !subprocess {
1832 test_env.add_config("git.subprocess = false");
1833 }
1834 test_env.add_config("git.auto-local-bookmark = true");
1835 let source_dir = test_env.work_dir("source");
1836 git::init(source_dir.root());
1837
1838 // Clone an empty repo. The target repo is a normal `jj` repo, *not* colocated
1839 let output = test_env.run_jj_in(".", ["git", "clone", "source", "target"]);
1840 insta::allow_duplicates! {
1841 insta::assert_snapshot!(output, @r#"
1842 ------- stderr -------
1843 Fetching into new repo in "$TEST_ENV/target"
1844 Nothing changed.
1845 [EOF]
1846 "#);
1847 }
1848 let target_dir = test_env.work_dir("target");
1849
1850 let source_log = create_colocated_repo_and_bookmarks_from_trunk1(&source_dir);
1851 insta::allow_duplicates! {
1852 insta::assert_snapshot!(source_log, @r#"
1853 ===== Source git repo contents =====
1854 @ 8cc4df9dd488 "b" b
1855 │ ○ e2a95b19b745 "a2" a2
1856 ├─╯
1857 │ ○ dd42071fe1ad "a1" a1
1858 ├─╯
1859 ○ 9929b494c411 "trunk1" trunk1
1860 ◆ 000000000000 ""
1861 [EOF]
1862 "#);
1863 }
1864
1865 // Fetch all bookmarks
1866 let output = target_dir.run_jj(["git", "fetch"]);
1867 insta::allow_duplicates! {
1868 insta::assert_snapshot!(output, @r"
1869 ------- stderr -------
1870 bookmark: a1@origin [new] tracked
1871 bookmark: a2@origin [new] tracked
1872 bookmark: b@origin [new] tracked
1873 bookmark: trunk1@origin [new] tracked
1874 [EOF]
1875 ");
1876 }
1877 insta::allow_duplicates! {
1878 insta::assert_snapshot!(get_log_output(&target_dir), @r#"
1879 @ 230dd059e1b0 ""
1880 │ ○ 8cc4df9dd488 "b" b
1881 │ │ ○ e2a95b19b745 "a2" a2
1882 │ ├─╯
1883 │ │ ○ dd42071fe1ad "a1" a1
1884 │ ├─╯
1885 │ ○ 9929b494c411 "trunk1" trunk1
1886 ├─╯
1887 ◆ 000000000000 ""
1888 [EOF]
1889 "#);
1890 }
1891
1892 // Remove all bookmarks in origin.
1893 source_dir
1894 .run_jj(["bookmark", "forget", "--include-remotes", "glob:*"])
1895 .success();
1896
1897 // Fetch bookmarks master, trunk1 and a1 from origin and check that only those
1898 // bookmarks have been removed and that others were not rebased because of
1899 // abandoned commits.
1900 let output = target_dir.run_jj([
1901 "git", "fetch", "--branch", "master", "--branch", "trunk1", "--branch", "a1",
1902 ]);
1903 insta::allow_duplicates! {
1904 insta::assert_snapshot!(output, @r"
1905 ------- stderr -------
1906 bookmark: a1@origin [deleted] untracked
1907 bookmark: trunk1@origin [deleted] untracked
1908 Abandoned 1 commits that are no longer reachable.
1909 Warning: No branch matching `master` found on any specified/configured remote
1910 [EOF]
1911 ");
1912 }
1913 insta::allow_duplicates! {
1914 insta::assert_snapshot!(get_log_output(&target_dir), @r#"
1915 @ 230dd059e1b0 ""
1916 │ ○ 8cc4df9dd488 "b" b
1917 │ │ ○ e2a95b19b745 "a2" a2
1918 │ ├─╯
1919 │ ○ 9929b494c411 "trunk1"
1920 ├─╯
1921 ◆ 000000000000 ""
1922 [EOF]
1923 "#);
1924 }
1925}
1926
1927#[cfg_attr(feature = "git2", test_case(false; "use git2 for remote calls"))]
1928#[test_case(true; "spawn a git subprocess for remote calls")]
1929fn test_git_fetch_remote_only_bookmark(subprocess: bool) {
1930 let test_env = TestEnvironment::default();
1931 if !subprocess {
1932 test_env.add_config("git.subprocess = false");
1933 }
1934 test_env.run_jj_in(".", ["git", "init", "repo"]).success();
1935 let work_dir = test_env.work_dir("repo");
1936
1937 // Create non-empty git repo to add as a remote
1938 let git_repo_path = test_env.env_root().join("git-repo");
1939 let git_repo = git::init(git_repo_path);
1940 work_dir
1941 .run_jj(["git", "remote", "add", "origin", "../git-repo"])
1942 .success();
1943
1944 // Create a commit and a bookmark in the git repo
1945 let commit_result = git::add_commit(
1946 &git_repo,
1947 "refs/heads/feature1",
1948 "file",
1949 b"content",
1950 "message",
1951 &[],
1952 );
1953
1954 // Fetch using git.auto_local_bookmark = true
1955 test_env.add_config("git.auto-local-bookmark = true");
1956 work_dir
1957 .run_jj(["git", "fetch", "--remote=origin"])
1958 .success();
1959 insta::allow_duplicates! {
1960 insta::assert_snapshot!(get_bookmark_output(&work_dir), @r"
1961 feature1: qomsplrm ebeb70d8 message
1962 @origin: qomsplrm ebeb70d8 message
1963 [EOF]
1964 ");
1965 }
1966
1967 git::write_commit(
1968 &git_repo,
1969 "refs/heads/feature2",
1970 commit_result.tree_id,
1971 "message",
1972 &[],
1973 );
1974
1975 // Fetch using git.auto_local_bookmark = false
1976 test_env.add_config("git.auto-local-bookmark = false");
1977 work_dir
1978 .run_jj(["git", "fetch", "--remote=origin"])
1979 .success();
1980 insta::allow_duplicates! {
1981 insta::assert_snapshot!(get_log_output(&work_dir), @r#"
1982 @ 230dd059e1b0 ""
1983 │ ◆ ebeb70d8c5f9 "message" feature1 feature2@origin
1984 ├─╯
1985 ◆ 000000000000 ""
1986 [EOF]
1987 "#);
1988 }
1989 insta::allow_duplicates! {
1990 insta::assert_snapshot!(get_bookmark_output(&work_dir), @r"
1991 feature1: qomsplrm ebeb70d8 message
1992 @origin: qomsplrm ebeb70d8 message
1993 feature2@origin: qomsplrm ebeb70d8 message
1994 [EOF]
1995 ");
1996 }
1997}
1998
1999#[cfg_attr(feature = "git2", test_case(false; "use git2 for remote calls"))]
2000#[test_case(true; "spawn a git subprocess for remote calls")]
2001fn test_git_fetch_preserve_commits_across_repos(subprocess: bool) {
2002 let test_env = TestEnvironment::default();
2003 if !subprocess {
2004 test_env.add_config("git.subprocess = false");
2005 }
2006 test_env.add_config("git.auto-local-bookmark = true");
2007 test_env.run_jj_in(".", ["git", "init", "repo"]).success();
2008 let work_dir = test_env.work_dir("repo");
2009
2010 let upstream_repo = add_git_remote(&test_env, &work_dir, "upstream");
2011
2012 let fork_path = test_env.env_root().join("fork");
2013 let fork_repo = clone_git_remote_into(&test_env, "upstream", "fork");
2014 work_dir
2015 .run_jj(["git", "remote", "add", "fork", "../fork"])
2016 .success();
2017
2018 // add commit to fork remote in another branch
2019 add_commit_to_branch(&fork_repo, "feature");
2020
2021 // fetch remote bookmarks
2022 work_dir
2023 .run_jj(["git", "fetch", "--remote=fork", "--remote=upstream"])
2024 .success();
2025 insta::allow_duplicates! {
2026 insta::assert_snapshot!(get_log_output(&work_dir), @r#"
2027 @ 230dd059e1b0 ""
2028 │ ○ bcd7cd779791 "message" upstream
2029 ├─╯
2030 │ ○ 16ec9ef2877a "message" feature
2031 ├─╯
2032 ◆ 000000000000 ""
2033 [EOF]
2034 "#);
2035 }
2036 insta::allow_duplicates! {
2037 insta::assert_snapshot!(get_bookmark_output(&work_dir), @r"
2038 feature: srwrtuky 16ec9ef2 message
2039 @fork: srwrtuky 16ec9ef2 message
2040 upstream: zkvzklqn bcd7cd77 message
2041 @fork: zkvzklqn bcd7cd77 message
2042 @upstream: zkvzklqn bcd7cd77 message
2043 [EOF]
2044 ");
2045 }
2046
2047 // merge fork/feature into the upstream/upstream
2048 git::add_remote(upstream_repo.git_dir(), "fork", fork_path.to_str().unwrap());
2049 git::fetch(upstream_repo.git_dir(), "fork");
2050
2051 let base_id = upstream_repo
2052 .find_reference("refs/heads/upstream")
2053 .unwrap()
2054 .peel_to_commit()
2055 .unwrap()
2056 .id()
2057 .detach();
2058
2059 let fork_id = upstream_repo
2060 .find_reference("refs/remotes/fork/feature")
2061 .unwrap()
2062 .peel_to_commit()
2063 .unwrap()
2064 .id()
2065 .detach();
2066
2067 git::write_commit(
2068 &upstream_repo,
2069 "refs/heads/upstream",
2070 upstream_repo.empty_tree().id().detach(),
2071 "merge",
2072 &[base_id, fork_id],
2073 );
2074
2075 // remove branch on the fork
2076 fork_repo
2077 .find_reference("refs/heads/feature")
2078 .unwrap()
2079 .delete()
2080 .unwrap();
2081
2082 // fetch again on the jj repo, first looking at fork and then at upstream
2083 work_dir
2084 .run_jj(["git", "fetch", "--remote=fork", "--remote=upstream"])
2085 .success();
2086 insta::allow_duplicates! {
2087 insta::assert_snapshot!(get_log_output(&work_dir), @r#"
2088 @ 230dd059e1b0 ""
2089 │ ○ f3e9250bd003 "merge" upstream*
2090 │ ├─╮
2091 │ │ ○ 16ec9ef2877a "message"
2092 ├───╯
2093 │ ○ bcd7cd779791 "message" upstream@fork
2094 ├─╯
2095 ◆ 000000000000 ""
2096 [EOF]
2097 "#);
2098 }
2099 insta::allow_duplicates! {
2100 insta::assert_snapshot!(get_bookmark_output(&work_dir), @r"
2101 upstream: trrkvuqr f3e9250b merge
2102 @fork (behind by 2 commits): zkvzklqn bcd7cd77 message
2103 @upstream: trrkvuqr f3e9250b merge
2104 [EOF]
2105 ");
2106 }
2107}
2108
2109// TODO: Remove with the `git.subprocess` setting.
2110#[cfg(not(feature = "git2"))]
2111#[test]
2112fn test_git_fetch_git2_warning() {
2113 let test_env = TestEnvironment::default();
2114 test_env.add_config("git.subprocess = false");
2115 test_env.add_config("git.auto-local-bookmark = true");
2116 test_env.run_jj_in(".", ["git", "init", "repo"]).success();
2117 let work_dir = test_env.work_dir("repo");
2118 add_git_remote(&test_env, &work_dir, "origin");
2119
2120 let output = work_dir.run_jj(["git", "fetch"]);
2121 insta::assert_snapshot!(output, @r#"
2122 ------- stderr -------
2123 Warning: Deprecated config: jj was compiled without `git.subprocess = false` support
2124 bookmark: origin@origin [new] tracked
2125 [EOF]
2126 "#);
2127}