From 75fa7de0016bb761ed87cb9972cd99ce14fc2247 Mon Sep 17 00:00:00 2001 From: Ivan Petkov Date: Sat, 16 Aug 2025 16:21:10 -0700 Subject: [PATCH] cli: git fetch: fall back to remote fetch refspecs when -b not specified This changes the behavior of git fetch to respect the fetch refspecs configured on the remote. This is handy for projects which use customized fetch refspecs (e.g. only fetch certain patterns, but not all branches) but without having to remember and repeat all the patterns by hand on the CLI Fixes #5323 --- CHANGELOG.md | 5 ++ cli/src/commands/git/fetch.rs | 79 +++++++++++++++++++------------- cli/src/commands/git/mod.rs | 1 + cli/tests/cli-reference@.md.snap | 2 - cli/tests/test_git_fetch.rs | 50 ++++++++++++++++++++ 5 files changed, 103 insertions(+), 34 deletions(-) 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();