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
This commit is contained in:
Ivan Petkov 2025-08-16 16:21:10 -07:00
parent 4563621f89
commit 75fa7de001
5 changed files with 103 additions and 34 deletions

View file

@ -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

View file

@ -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<StringPattern>)> = 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(())
}

View file

@ -21,6 +21,7 @@ mod push;
mod remote;
mod root;
use std::io::Write as _;
use std::path::Path;
use clap::Subcommand;

View file

@ -1259,8 +1259,6 @@ If a working-copy commit gets abandoned, it will be given a new, empty commit. T
* `-b`, `--branch <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).

View file

@ -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();