diff --git a/CHANGELOG.md b/CHANGELOG.md index cf3b773b9..759b5120d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,6 +31,11 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). many people to be negatively affected, because running `jj undo` twice was previously a no-op. +* `jj git fetch` will now only fetch the refspec patterns configured on remotes + when the `--bookmark` option is omitted. Only simple refspec patterns + are currently supported, and anything else (like refspecs which rename + branches) will be ignored. + ### Deprecations * The on-disk index format has changed. `jj` will write index files in both old diff --git a/cli/src/commands/git/fetch.rs b/cli/src/commands/git/fetch.rs index e41b9045a..4aba19055 100644 --- a/cli/src/commands/git/fetch.rs +++ b/cli/src/commands/git/fetch.rs @@ -19,7 +19,11 @@ use itertools::Itertools as _; use jj_lib::config::ConfigGetResultExt as _; use jj_lib::git; use jj_lib::git::GitFetch; +use jj_lib::git::IgnoredRefspec; +use jj_lib::git::IgnoredRefspecs; +use jj_lib::git::expand_default_fetch_refspecs; use jj_lib::git::expand_fetch_refspecs; +use jj_lib::git::get_git_backend; use jj_lib::ref_name::RemoteName; use jj_lib::repo::Repo as _; use jj_lib::str_util::StringPattern; @@ -51,7 +55,6 @@ pub struct GitFetchArgs { #[arg( long, short, alias = "bookmark", - default_value = "glob:*", value_parser = StringPattern::parse, add = ArgValueCandidates::new(complete::bookmarks), )] @@ -126,44 +129,40 @@ pub fn cmd_git_fetch( .sorted() .collect_vec(); - let branches_by_remote: Vec<(&RemoteName, Vec)> = if args.tracked { - remotes - .iter() - .map(|&remote| { - let tracked_branches = workspace_command - .repo() - .view() - .local_remote_bookmarks(remote) - .filter(|(_, targets)| targets.remote_ref.is_tracked()) - .map(|(name, _)| StringPattern::exact(name)) - .collect_vec(); - (remote, tracked_branches) - }) - .collect_vec() + let mut tx = workspace_command.start_transaction(); + + let mut expansions = Vec::with_capacity(remotes.len()); + if args.tracked { + for remote in &remotes { + let tracked_branches = tx + .repo() + .view() + .local_remote_bookmarks(remote) + .filter(|(_, targets)| targets.remote_ref.is_tracked()) + .map(|(name, _)| StringPattern::exact(name)) + .collect_vec(); + expansions.push((remote, expand_fetch_refspecs(remote, tracked_branches)?)); + } + } else if args.branch.is_empty() { + let git_repo = get_git_backend(tx.repo_mut().store())?.git_repo(); + for remote in &remotes { + let (ignored, expanded) = expand_default_fetch_refspecs(remote, &git_repo)?; + warn_ignored_refspecs(ui, remote, ignored)?; + expansions.push((remote, expanded)); + } } else { - remotes - .iter() - .map(|&remote| (remote, args.branch.clone())) - .collect_vec() + for remote in &remotes { + let expanded = expand_fetch_refspecs(remote, args.branch.clone())?; + expansions.push((remote, expanded)); + } }; - let mut tx = workspace_command.start_transaction(); let git_settings = tx.settings().git_settings()?; let mut git_fetch = GitFetch::new(tx.repo_mut(), &git_settings)?; - for (remote, branches) in branches_by_remote { - // Skip remotes with no branches to fetch - if branches.is_empty() { - continue; - } + for (remote, expanded) in expansions { with_remote_git_callbacks(ui, |callbacks| { - git_fetch.fetch( - remote, - expand_fetch_refspecs(remote, branches)?, - callbacks, - None, - None, - ) + git_fetch.fetch(remote, expanded, callbacks, None, None) })?; } @@ -251,3 +250,19 @@ fn warn_if_branches_not_found( Ok(()) } + +fn warn_ignored_refspecs( + ui: &Ui, + remote_name: &RemoteName, + IgnoredRefspecs(ignored_refspecs): IgnoredRefspecs, +) -> Result<(), CommandError> { + let remote_name = remote_name.as_symbol(); + for IgnoredRefspec { refspec, reason } in ignored_refspecs { + writeln!( + ui.warning_default(), + "Ignored refspec `{refspec}` from `{remote_name}`: {reason}", + )?; + } + + Ok(()) +} diff --git a/cli/src/commands/git/mod.rs b/cli/src/commands/git/mod.rs index 903a7e7cc..274778b23 100644 --- a/cli/src/commands/git/mod.rs +++ b/cli/src/commands/git/mod.rs @@ -21,6 +21,7 @@ mod push; mod remote; mod root; +use std::io::Write as _; use std::path::Path; use clap::Subcommand; diff --git a/cli/tests/cli-reference@.md.snap b/cli/tests/cli-reference@.md.snap index f78cdc615..30cd7b7b9 100644 --- a/cli/tests/cli-reference@.md.snap +++ b/cli/tests/cli-reference@.md.snap @@ -1259,8 +1259,6 @@ If a working-copy commit gets abandoned, it will be given a new, empty commit. T * `-b`, `--branch ` — Fetch only some of the branches By default, the specified name matches exactly. Use `glob:` prefix to expand `*` as a glob, e.g. `--branch 'glob:push-*'`. Other wildcard characters such as `?` are *not* supported. Can be repeated to specify multiple branches. - - Default value: `glob:*` * `--tracked` — Fetch only tracked bookmarks This fetches only bookmarks that are already tracked from the specified remote(s). diff --git a/cli/tests/test_git_fetch.rs b/cli/tests/test_git_fetch.rs index 4ac7162bf..76182236c 100644 --- a/cli/tests/test_git_fetch.rs +++ b/cli/tests/test_git_fetch.rs @@ -12,6 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. +use std::io::Write as _; + use testutils::git; use crate::common::CommandOutput; @@ -228,6 +230,54 @@ fn test_git_fetch_multiple_remotes() { "); } +#[test] +fn test_git_fetch_with_ignored_refspecs() { + let test_env = TestEnvironment::default(); + test_env.add_config("git.auto-local-bookmark = true"); + test_env + .run_jj_in(".", ["git", "init", "--colocate", "repo"]) + .success(); + let source_repo = init_git_remote(&test_env, "origin"); + + for branch in ["main", "foo", "foobar", "foobaz", "bar"] { + add_commit_to_branch(&source_repo, &format!("refs/heads/{branch}"), branch); + } + + let work_dir = test_env.work_dir("repo"); + std::fs::OpenOptions::new() + .append(true) + .open(work_dir.root().join("./.git/config")) + .expect("failed to open config file") + .write_all( + br#" + [remote "origin"] + url = ../origin/.git + fetch = +refs/heads/main:refs/remotes/origin/main + fetch = +refs/heads/foo*:refs/remotes/origin/baz* + fetch = +refs/heads/bar*:refs/tags/bar* + fetch = refs/heads/bar + "#, + ) + .expect("failed to update config file"); + + let output = work_dir.run_jj(["git", "fetch"]).success(); + + insta::assert_snapshot!(output.stdout, @r""); + insta::assert_snapshot!(output.stderr, @r" + Warning: Ignored refspec `refs/heads/bar` from `origin`: fetch-only refspecs are not supported + Warning: Ignored refspec `+refs/heads/bar*:refs/tags/bar*` from `origin`: only refs/remotes/ is supported for fetch destinations + Warning: Ignored refspec `+refs/heads/foo*:refs/remotes/origin/baz*` from `origin`: renaming is not supported + bookmark: main@origin [new] tracked + [EOF] + "); + insta::assert_snapshot!(get_bookmark_output(&work_dir), @r" + main: kpukqsrq f803461e main + @git: kpukqsrq f803461e main + @origin: kpukqsrq f803461e main + [EOF] + "); +} + #[test] fn test_git_fetch_with_glob() { let test_env = TestEnvironment::default();