From 30d03a66e67b14eaf99bfeff60e5f46f6971f379 Mon Sep 17 00:00:00 2001 From: Ilya Grigoriev Date: Thu, 12 Jan 2023 14:53:26 -0800 Subject: [PATCH] cmd: `--branch` option for `git fetch`. Thanks to @samueltardieu for noticing a subtle bug in the refspecs, providing the fix, as well as the two `conflicting_branches` tests. --- CHANGELOG.md | 3 + lib/src/git.rs | 15 +- lib/tests/test_git.rs | 9 + src/commands/git.rs | 11 + tests/test_git_fetch.rs | 483 ++++++++++++++++++++++++++++++++++++++++ 5 files changed, 519 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 53b77ef89..1e6caf087 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * `jj rebase` now accepts multiple `-s` and `-b` arguments. Revsets with multiple commits are allowed with `--allow-large-revsets`. +* `jj git fetch` now supports a `--branch` argument to fetch some of the + branches only. + ### Fixed bugs * Modify/delete conflicts now include context lines diff --git a/lib/src/git.rs b/lib/src/git.rs index 5cb79158a..41220a06a 100644 --- a/lib/src/git.rs +++ b/lib/src/git.rs @@ -330,6 +330,8 @@ pub fn export_refs( pub enum GitFetchError { #[error("No git remote named '{0}'")] NoSuchRemote(String), + #[error("Invalid glob provided. Globs may not contain the characters `:` or `^`.")] + InvalidGlob, // TODO: I'm sure there are other errors possible, such as transport-level errors. #[error("Unexpected git error when fetching: {0}")] InternalGitError(#[from] git2::Error), @@ -340,6 +342,7 @@ pub fn fetch( mut_repo: &mut MutableRepo, git_repo: &git2::Repository, remote_name: &str, + globs: &[String], callbacks: RemoteCallbacks<'_>, git_settings: &GitSettings, ) -> Result, GitFetchError> { @@ -361,9 +364,17 @@ pub fn fetch( fetch_options.proxy_options(proxy_options); let callbacks = callbacks.into_git(); fetch_options.remote_callbacks(callbacks); - let refspec: &[&str] = &[]; + if globs.iter().any(|g| g.contains(|c| ":^".contains(c))) { + return Err(GitFetchError::InvalidGlob); + } + // At this point, we are only updating Git's remote tracking branches, not the + // local branches. + let refspecs = globs + .iter() + .map(|glob| format!("+refs/heads/{glob}:refs/remotes/{remote_name}/{glob}")) + .collect_vec(); tracing::debug!("remote.download"); - remote.download(refspec, Some(&mut fetch_options))?; + remote.download(&refspecs, Some(&mut fetch_options))?; tracing::debug!("remote.prune"); remote.prune(None)?; tracing::debug!("remote.update_tips"); diff --git a/lib/tests/test_git.rs b/lib/tests/test_git.rs index ec26df066..8d2e2c9ff 100644 --- a/lib/tests/test_git.rs +++ b/lib/tests/test_git.rs @@ -957,6 +957,7 @@ fn test_fetch_empty_repo() { tx.mut_repo(), &test_data.git_repo, "origin", + &[], git::RemoteCallbacks::default(), &git_settings, ) @@ -980,6 +981,7 @@ fn test_fetch_initial_commit() { tx.mut_repo(), &test_data.git_repo, "origin", + &[], git::RemoteCallbacks::default(), &git_settings, ) @@ -1021,6 +1023,7 @@ fn test_fetch_success() { tx.mut_repo(), &test_data.git_repo, "origin", + &[], git::RemoteCallbacks::default(), &git_settings, ) @@ -1041,6 +1044,7 @@ fn test_fetch_success() { tx.mut_repo(), &test_data.git_repo, "origin", + &[], git::RemoteCallbacks::default(), &git_settings, ) @@ -1082,6 +1086,7 @@ fn test_fetch_prune_deleted_ref() { tx.mut_repo(), &test_data.git_repo, "origin", + &[], git::RemoteCallbacks::default(), &git_settings, ) @@ -1100,6 +1105,7 @@ fn test_fetch_prune_deleted_ref() { tx.mut_repo(), &test_data.git_repo, "origin", + &[], git::RemoteCallbacks::default(), &git_settings, ) @@ -1120,6 +1126,7 @@ fn test_fetch_no_default_branch() { tx.mut_repo(), &test_data.git_repo, "origin", + &[], git::RemoteCallbacks::default(), &git_settings, ) @@ -1142,6 +1149,7 @@ fn test_fetch_no_default_branch() { tx.mut_repo(), &test_data.git_repo, "origin", + &[], git::RemoteCallbacks::default(), &git_settings, ) @@ -1161,6 +1169,7 @@ fn test_fetch_no_such_remote() { tx.mut_repo(), &test_data.git_repo, "invalid-remote", + &[], git::RemoteCallbacks::default(), &git_settings, ); diff --git a/src/commands/git.rs b/src/commands/git.rs index e2898fae1..c44007041 100644 --- a/src/commands/git.rs +++ b/src/commands/git.rs @@ -85,6 +85,12 @@ pub struct GitRemoteListArgs {} /// Fetch from a Git remote #[derive(clap::Args, Clone, Debug)] pub struct GitFetchArgs { + /// Fetch only some of the branches (caution: known bugs) + /// + /// Any `*` in the argument is expanded as a glob. So, one `--branch` can + /// match several branches. + #[arg(long)] + branch: Vec, /// The remote to fetch from (only named remotes are supported, can be /// repeated) #[arg(long = "remote", value_name = "remote")] @@ -263,6 +269,7 @@ fn cmd_git_fetch( tx.mut_repo(), &git_repo, &remote, + &args.branch, cb, &command.settings().git_settings(), ) @@ -390,6 +397,7 @@ fn do_git_clone( fetch_tx.mut_repo(), &git_repo, remote_name, + &[], cb, &command.settings().git_settings(), ) @@ -399,6 +407,9 @@ fn do_git_clone( panic!("shouldn't happen as we just created the git remote") } GitFetchError::InternalGitError(err) => user_error(format!("Fetch failed: {err}")), + GitFetchError::InvalidGlob => { + unreachable!("we didn't provide any globs") + } })?; fetch_tx.finish(ui)?; Ok((workspace_command, maybe_default_branch)) diff --git a/tests/test_git_fetch.rs b/tests/test_git_fetch.rs index e77ba80ef..fb8f7b79b 100644 --- a/tests/test_git_fetch.rs +++ b/tests/test_git_fetch.rs @@ -50,6 +50,32 @@ fn get_branch_output(test_env: &TestEnvironment, repo_path: &Path) -> String { test_env.jj_cmd_success(repo_path, &["branch", "list"]) } +fn create_commit(test_env: &TestEnvironment, repo_path: &Path, name: &str, parents: &[&str]) { + let descr = format!("descr_for_{name}"); + if parents.is_empty() { + test_env.jj_cmd_success(repo_path, &["new", "root", "-m", &descr]); + } else { + let mut args = vec!["new", "-m", &descr]; + args.extend(parents); + test_env.jj_cmd_success(repo_path, &args); + } + std::fs::write(repo_path.join(name), format!("{name}\n")).unwrap(); + test_env.jj_cmd_success(repo_path, &["branch", "create", name]); +} + +fn get_log_output(test_env: &TestEnvironment, workspace_root: &Path) -> String { + test_env.jj_cmd_success( + workspace_root, + &[ + "log", + "-T", + r#"commit_id.short() " " description.first_line() " " branches"#, + "-r", + "all()", + ], + ) +} + #[test] fn test_git_fetch_default_remote() { let test_env = TestEnvironment::default(); @@ -182,3 +208,460 @@ fn test_git_fetch_prune_before_updating_tips() { origin/subname: 9f01a0e04879 message "###); } + +#[test] +fn test_git_fetch_conflicting_branches() { + let test_env = TestEnvironment::default(); + test_env.jj_cmd_success(test_env.env_root(), &["init", "repo", "--git"]); + let repo_path = test_env.env_root().join("repo"); + add_git_remote(&test_env, &repo_path, "rem1"); + + // Create a rem1 branch locally + test_env.jj_cmd_success(&repo_path, &["new", "root"]); + test_env.jj_cmd_success(&repo_path, &["branch", "create", "rem1"]); + insta::assert_snapshot!(get_branch_output(&test_env, &repo_path), @r###" + rem1: fcdbbd731496 (no description set) + "###); + + test_env.jj_cmd_success( + &repo_path, + &["git", "fetch", "--remote", "rem1", "--branch", "*"], + ); + // This should result in a CONFLICTED branch + insta::assert_snapshot!(get_branch_output(&test_env, &repo_path), @r###" + rem1 (conflicted): + + fcdbbd731496 (no description set) + + 9f01a0e04879 message + @rem1 (behind by 1 commits): 9f01a0e04879 message + "###); +} + +#[test] +fn test_git_fetch_conflicting_branches_colocated() { + let test_env = TestEnvironment::default(); + let repo_path = test_env.env_root().join("repo"); + let _git_repo = git2::Repository::init(&repo_path).unwrap(); + // create_colocated_repo_and_branches_from_trunk1(&test_env, &repo_path); + test_env.jj_cmd_success(&repo_path, &["init", "--git-repo", "."]); + add_git_remote(&test_env, &repo_path, "rem1"); + insta::assert_snapshot!(get_branch_output(&test_env, &repo_path), @""); + + // Create a rem1 branch locally + test_env.jj_cmd_success(&repo_path, &["new", "root"]); + test_env.jj_cmd_success(&repo_path, &["branch", "create", "rem1"]); + insta::assert_snapshot!(get_branch_output(&test_env, &repo_path), @r###" + rem1: f652c32197cf (no description set) + "###); + + test_env.jj_cmd_success( + &repo_path, + &["git", "fetch", "--remote", "rem1", "--branch", "rem1"], + ); + // This should result in a CONFLICTED branch + // See https://github.com/martinvonz/jj/pull/1146#discussion_r1112372340 for the bug this tests for. + insta::assert_snapshot!(get_branch_output(&test_env, &repo_path), @r###" + rem1 (conflicted): + + f652c32197cf (no description set) + + 9f01a0e04879 message + @rem1 (behind by 1 commits): 9f01a0e04879 message + "###); +} + +// Helper functions to test obtaining multiple branches at once and changed +// branches +fn create_colocated_repo_and_branches_from_trunk1( + test_env: &TestEnvironment, + repo_path: &Path, +) -> String { + // Create a colocated repo in `source` to populate it more easily + test_env.jj_cmd_success(repo_path, &["init", "--git-repo", "."]); + create_commit(test_env, repo_path, "trunk1", &[]); + create_commit(test_env, repo_path, "a1", &["trunk1"]); + create_commit(test_env, repo_path, "a2", &["trunk1"]); + create_commit(test_env, repo_path, "b", &["trunk1"]); + format!( + " ===== Source git repo contents =====\n{}", + get_log_output(test_env, repo_path) + ) +} + +fn create_trunk2_and_rebase_branches(test_env: &TestEnvironment, repo_path: &Path) -> String { + create_commit(test_env, repo_path, "trunk2", &["trunk1"]); + for br in ["a1", "a2", "b"] { + test_env.jj_cmd_success(repo_path, &["rebase", "-b", br, "-d", "trunk2"]); + } + format!( + " ===== Source git repo contents =====\n{}", + get_log_output(test_env, repo_path) + ) +} + +#[test] +fn test_git_fetch_all() { + let test_env = TestEnvironment::default(); + let source_git_repo_path = test_env.env_root().join("source"); + let _git_repo = git2::Repository::init(source_git_repo_path.clone()).unwrap(); + + // Clone an empty repo. The target repo is a normal `jj` repo, *not* colocated + let stdout = + test_env.jj_cmd_success(test_env.env_root(), &["git", "clone", "source", "target"]); + insta::assert_snapshot!(stdout, @r###" + Fetching into new repo in "$TEST_ENV/target" + Nothing changed. + "###); + let target_jj_repo_path = test_env.env_root().join("target"); + + let source_log = + create_colocated_repo_and_branches_from_trunk1(&test_env, &source_git_repo_path); + insta::assert_snapshot!(source_log, @r###" + ===== Source git repo contents ===== + @ c7d4bdcbc215 descr_for_b b + │ o decaa3966c83 descr_for_a2 a2 + ├─╯ + │ o 359a9a02457d descr_for_a1 a1 + ├─╯ + o ff36dc55760e descr_for_trunk1 master trunk1 + o 000000000000 + "###); + + // Nothing in our repo before the fetch + insta::assert_snapshot!(get_log_output(&test_env, &target_jj_repo_path), @r###" + @ 230dd059e1b0 + o 000000000000 + "###); + insta::assert_snapshot!(get_branch_output(&test_env, &target_jj_repo_path), @""); + insta::assert_snapshot!(test_env.jj_cmd_success(&target_jj_repo_path, &["git", "fetch"]), @""); + insta::assert_snapshot!(get_branch_output(&test_env, &target_jj_repo_path), @r###" + a1: 359a9a02457d descr_for_a1 + a2: decaa3966c83 descr_for_a2 + b: c7d4bdcbc215 descr_for_b + master: ff36dc55760e descr_for_trunk1 + trunk1: ff36dc55760e descr_for_trunk1 + "###); + insta::assert_snapshot!(get_log_output(&test_env, &target_jj_repo_path), @r###" + o c7d4bdcbc215 descr_for_b b + │ o decaa3966c83 descr_for_a2 a2 + ├─╯ + │ o 359a9a02457d descr_for_a1 a1 + ├─╯ + o ff36dc55760e descr_for_trunk1 master trunk1 + │ @ 230dd059e1b0 + ├─╯ + o 000000000000 + "###); + + // ==== Change both repos ==== + // First, change the target repo: + let source_log = create_trunk2_and_rebase_branches(&test_env, &source_git_repo_path); + insta::assert_snapshot!(source_log, @r###" + ===== Source git repo contents ===== + o babc49226c14 descr_for_b b + │ o 91e46b4b2653 descr_for_a2 a2 + ├─╯ + │ o 0424f6dfc1ff descr_for_a1 a1 + ├─╯ + @ 8f1f14fbbf42 descr_for_trunk2 trunk2 + o ff36dc55760e descr_for_trunk1 master trunk1 + o 000000000000 + "###); + // Change a branch in the source repo as well, so that it becomes conflicted. + test_env.jj_cmd_success( + &target_jj_repo_path, + &["describe", "b", "-m=new_descr_for_b_to_create_conflict"], + ); + + // Our repo before and after fetch + insta::assert_snapshot!(get_log_output(&test_env, &target_jj_repo_path), @r###" + o 061eddbb43ab new_descr_for_b_to_create_conflict b* + │ o decaa3966c83 descr_for_a2 a2 + ├─╯ + │ o 359a9a02457d descr_for_a1 a1 + ├─╯ + o ff36dc55760e descr_for_trunk1 master trunk1 + │ @ 230dd059e1b0 + ├─╯ + o 000000000000 + "###); + insta::assert_snapshot!(get_branch_output(&test_env, &target_jj_repo_path), @r###" + a1: 359a9a02457d descr_for_a1 + a2: decaa3966c83 descr_for_a2 + b: 061eddbb43ab new_descr_for_b_to_create_conflict + @origin (ahead by 1 commits, behind by 1 commits): c7d4bdcbc215 descr_for_b + master: ff36dc55760e descr_for_trunk1 + trunk1: ff36dc55760e descr_for_trunk1 + "###); + insta::assert_snapshot!(test_env.jj_cmd_success(&target_jj_repo_path, &["git", "fetch"]), @""); + insta::assert_snapshot!(get_branch_output(&test_env, &target_jj_repo_path), @r###" + a1: 0424f6dfc1ff descr_for_a1 + a2: 91e46b4b2653 descr_for_a2 + b (conflicted): + - c7d4bdcbc215 descr_for_b + + 061eddbb43ab new_descr_for_b_to_create_conflict + + babc49226c14 descr_for_b + @origin (behind by 1 commits): babc49226c14 descr_for_b + master: ff36dc55760e descr_for_trunk1 + trunk1: ff36dc55760e descr_for_trunk1 + trunk2: 8f1f14fbbf42 descr_for_trunk2 + "###); + insta::assert_snapshot!(get_log_output(&test_env, &target_jj_repo_path), @r###" + o babc49226c14 descr_for_b b?? b@origin + │ o 91e46b4b2653 descr_for_a2 a2 + ├─╯ + │ o 0424f6dfc1ff descr_for_a1 a1 + ├─╯ + o 8f1f14fbbf42 descr_for_trunk2 trunk2 + │ o 061eddbb43ab new_descr_for_b_to_create_conflict b?? + ├─╯ + o ff36dc55760e descr_for_trunk1 master trunk1 + │ @ 230dd059e1b0 + ├─╯ + o 000000000000 + "###); +} + +#[test] +fn test_git_fetch_some_of_many_branches() { + let test_env = TestEnvironment::default(); + let source_git_repo_path = test_env.env_root().join("source"); + let _git_repo = git2::Repository::init(source_git_repo_path.clone()).unwrap(); + + // Clone an empty repo. The target repo is a normal `jj` repo, *not* colocated + let stdout = + test_env.jj_cmd_success(test_env.env_root(), &["git", "clone", "source", "target"]); + insta::assert_snapshot!(stdout, @r###" + Fetching into new repo in "$TEST_ENV/target" + Nothing changed. + "###); + let target_jj_repo_path = test_env.env_root().join("target"); + + let source_log = + create_colocated_repo_and_branches_from_trunk1(&test_env, &source_git_repo_path); + insta::assert_snapshot!(source_log, @r###" + ===== Source git repo contents ===== + @ c7d4bdcbc215 descr_for_b b + │ o decaa3966c83 descr_for_a2 a2 + ├─╯ + │ o 359a9a02457d descr_for_a1 a1 + ├─╯ + o ff36dc55760e descr_for_trunk1 master trunk1 + o 000000000000 + "###); + + // Test an error message + let stderr = + test_env.jj_cmd_failure(&target_jj_repo_path, &["git", "fetch", "--branch", "^:a*"]); + insta::assert_snapshot!(stderr, @r###" + Error: Invalid glob provided. Globs may not contain the characters `:` or `^`. + "###); + + // Nothing in our repo before the fetch + insta::assert_snapshot!(get_log_output(&test_env, &target_jj_repo_path), @r###" + @ 230dd059e1b0 + o 000000000000 + "###); + // Fetch one branch... + let stdout = test_env.jj_cmd_success(&target_jj_repo_path, &["git", "fetch", "--branch", "b"]); + insta::assert_snapshot!(stdout, @""); + insta::assert_snapshot!(get_log_output(&test_env, &target_jj_repo_path), @r###" + o c7d4bdcbc215 descr_for_b b + o ff36dc55760e descr_for_trunk1 + │ @ 230dd059e1b0 + ├─╯ + o 000000000000 + "###); + // ...check what the intermediate state looks like... + insta::assert_snapshot!(get_branch_output(&test_env, &target_jj_repo_path), @r###" + b: c7d4bdcbc215 descr_for_b + "###); + // ...then fetch two others with a glob. + let stdout = test_env.jj_cmd_success(&target_jj_repo_path, &["git", "fetch", "--branch", "a*"]); + insta::assert_snapshot!(stdout, @""); + insta::assert_snapshot!(get_log_output(&test_env, &target_jj_repo_path), @r###" + o decaa3966c83 descr_for_a2 a2 + │ o 359a9a02457d descr_for_a1 a1 + ├─╯ + │ o c7d4bdcbc215 descr_for_b b + ├─╯ + o ff36dc55760e descr_for_trunk1 + │ @ 230dd059e1b0 + ├─╯ + o 000000000000 + "###); + // Fetching the same branch again + let stdout = test_env.jj_cmd_success(&target_jj_repo_path, &["git", "fetch", "--branch", "a1"]); + insta::assert_snapshot!(stdout, @r###" + Nothing changed. + "###); + insta::assert_snapshot!(get_log_output(&test_env, &target_jj_repo_path), @r###" + o decaa3966c83 descr_for_a2 a2 + │ o 359a9a02457d descr_for_a1 a1 + ├─╯ + │ o c7d4bdcbc215 descr_for_b b + ├─╯ + o ff36dc55760e descr_for_trunk1 + │ @ 230dd059e1b0 + ├─╯ + o 000000000000 + "###); + + // ==== Change both repos ==== + // First, change the target repo: + let source_log = create_trunk2_and_rebase_branches(&test_env, &source_git_repo_path); + insta::assert_snapshot!(source_log, @r###" + ===== Source git repo contents ===== + o 13ac032802f1 descr_for_b b + │ o 010977d69c5b descr_for_a2 a2 + ├─╯ + │ o 6f4e1c4dfe29 descr_for_a1 a1 + ├─╯ + @ 09430ba04a82 descr_for_trunk2 trunk2 + o ff36dc55760e descr_for_trunk1 master trunk1 + o 000000000000 + "###); + // Change a branch in the source repo as well, so that it becomes conflicted. + test_env.jj_cmd_success( + &target_jj_repo_path, + &["describe", "b", "-m=new_descr_for_b_to_create_conflict"], + ); + + // Our repo before and after fetch of two branches + insta::assert_snapshot!(get_log_output(&test_env, &target_jj_repo_path), @r###" + o 2be688d8c664 new_descr_for_b_to_create_conflict b* + │ o decaa3966c83 descr_for_a2 a2 + ├─╯ + │ o 359a9a02457d descr_for_a1 a1 + ├─╯ + o ff36dc55760e descr_for_trunk1 + │ @ 230dd059e1b0 + ├─╯ + o 000000000000 + "###); + let stdout = test_env.jj_cmd_success( + &target_jj_repo_path, + &["git", "fetch", "--branch", "b", "--branch", "a1"], + ); + insta::assert_snapshot!(stdout, @""); + insta::assert_snapshot!(get_log_output(&test_env, &target_jj_repo_path), @r###" + o 13ac032802f1 descr_for_b b?? b@origin + │ o 6f4e1c4dfe29 descr_for_a1 a1 + ├─╯ + o 09430ba04a82 descr_for_trunk2 + │ o 2be688d8c664 new_descr_for_b_to_create_conflict b?? + ├─╯ + │ o decaa3966c83 descr_for_a2 a2 + ├─╯ + o ff36dc55760e descr_for_trunk1 + │ @ 230dd059e1b0 + ├─╯ + o 000000000000 + "###); + + // We left a2 where it was before, let's see how `jj branch list` sees this. + insta::assert_snapshot!(get_branch_output(&test_env, &target_jj_repo_path), @r###" + a1: 6f4e1c4dfe29 descr_for_a1 + a2: decaa3966c83 descr_for_a2 + b (conflicted): + - c7d4bdcbc215 descr_for_b + + 2be688d8c664 new_descr_for_b_to_create_conflict + + 13ac032802f1 descr_for_b + @origin (behind by 1 commits): 13ac032802f1 descr_for_b + "###); + // Now, let's fetch a2 and double-check that fetching a1 and b again doesn't do + // anything. + let stdout = test_env.jj_cmd_success( + &target_jj_repo_path, + &["git", "fetch", "--branch", "b", "--branch", "a*"], + ); + insta::assert_snapshot!(stdout, @""); + insta::assert_snapshot!(get_log_output(&test_env, &target_jj_repo_path), @r###" + o 010977d69c5b descr_for_a2 a2 + │ o 13ac032802f1 descr_for_b b?? b@origin + ├─╯ + │ o 6f4e1c4dfe29 descr_for_a1 a1 + ├─╯ + o 09430ba04a82 descr_for_trunk2 + │ o 2be688d8c664 new_descr_for_b_to_create_conflict b?? + ├─╯ + o ff36dc55760e descr_for_trunk1 + │ @ 230dd059e1b0 + ├─╯ + o 000000000000 + "###); + insta::assert_snapshot!(get_branch_output(&test_env, &target_jj_repo_path), @r###" + a1: 6f4e1c4dfe29 descr_for_a1 + a2: 010977d69c5b descr_for_a2 + b (conflicted): + - c7d4bdcbc215 descr_for_b + + 2be688d8c664 new_descr_for_b_to_create_conflict + + 13ac032802f1 descr_for_b + @origin (behind by 1 commits): 13ac032802f1 descr_for_b + "###); +} + +// TODO: Fix the bug this test demonstrates. (https://github.com/martinvonz/jj/issues/1300) +// The issue likely stems from the fact that `jj undo` does not undo the fetch +// inside the git repo backing the `target` repo. It is unclear whether it +// should. +#[test] +fn test_git_fetch_undo() { + let test_env = TestEnvironment::default(); + let source_git_repo_path = test_env.env_root().join("source"); + let _git_repo = git2::Repository::init(source_git_repo_path.clone()).unwrap(); + + // Clone an empty repo. The target repo is a normal `jj` repo, *not* colocated + let stdout = + test_env.jj_cmd_success(test_env.env_root(), &["git", "clone", "source", "target"]); + insta::assert_snapshot!(stdout, @r###" + Fetching into new repo in "$TEST_ENV/target" + Nothing changed. + "###); + let target_jj_repo_path = test_env.env_root().join("target"); + + let source_log = + create_colocated_repo_and_branches_from_trunk1(&test_env, &source_git_repo_path); + insta::assert_snapshot!(source_log, @r###" + ===== Source git repo contents ===== + @ c7d4bdcbc215 descr_for_b b + │ o decaa3966c83 descr_for_a2 a2 + ├─╯ + │ o 359a9a02457d descr_for_a1 a1 + ├─╯ + o ff36dc55760e descr_for_trunk1 master trunk1 + o 000000000000 + "###); + + // Fetch 2 branches + let stdout = test_env.jj_cmd_success( + &target_jj_repo_path, + &["git", "fetch", "--branch", "b", "--branch", "a1"], + ); + insta::assert_snapshot!(stdout, @""); + insta::assert_snapshot!(get_log_output(&test_env, &target_jj_repo_path), @r###" + o c7d4bdcbc215 descr_for_b b + │ o 359a9a02457d descr_for_a1 a1 + ├─╯ + o ff36dc55760e descr_for_trunk1 + │ @ 230dd059e1b0 + ├─╯ + o 000000000000 + "###); + insta::assert_snapshot!(test_env.jj_cmd_success(&target_jj_repo_path, &["undo"]), @""); + // The undo works as expected + insta::assert_snapshot!(get_log_output(&test_env, &target_jj_repo_path), @r###" + @ 230dd059e1b0 + o 000000000000 + "###); + // Now try to fetch just one branch + let stdout = test_env.jj_cmd_success(&target_jj_repo_path, &["git", "fetch", "--branch", "b"]); + insta::assert_snapshot!(stdout, @""); + // BUG: Both branches got fetched. + insta::assert_snapshot!(get_log_output(&test_env, &target_jj_repo_path), @r###" + o c7d4bdcbc215 descr_for_b b + │ o 359a9a02457d descr_for_a1 a1 + ├─╯ + o ff36dc55760e descr_for_trunk1 + │ @ 230dd059e1b0 + ├─╯ + o 000000000000 + "###); +}