git: tolerate unknown git.fetch remotes if others are available

If `git.fetch` contains remotes that are not available, we currently error even
if other remotes are available. For common fork workflows with separate
`upstream` and `origin` remotes (for example), this requires a user to either
set both remotes in their user config and override single-remote repos or set
only one in their user config and override all multi-remote repos to fetch from
`upstream` (or both).

This change updates fetching to only *warn* about unknown remotes **if** other
remotes are available. If none of the configured remotes are available, an error
is still raised as before.
This commit is contained in:
Jacob Hayes 2025-04-24 17:09:28 -05:00
parent 92629ded4c
commit 1cdd79071e
3 changed files with 44 additions and 24 deletions

View File

@ -61,6 +61,9 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
* Added `ui.streampager.show-ruler` setting to configure whether the ruler should be
shown when the builtin pager starts up.
* `jj git fetch` now warns instead of erroring for unknown `git.fetch` remotes
if other remotes are available.
### Fixed bugs
* Fixed crash on change-delete conflict resolution.

View File

@ -98,36 +98,26 @@ pub fn cmd_git_fetch(
let all_remotes = git::get_all_remote_names(workspace_command.repo().store())?;
let mut matching_remotes = HashSet::new();
let mut unmatched_patterns = Vec::new();
for pattern in remote_patterns {
let remotes = all_remotes
.iter()
.filter(|r| pattern.matches(r.as_str()))
.collect_vec();
if remotes.is_empty() {
unmatched_patterns.push(pattern);
writeln!(ui.warning_default(), "No git remotes matching '{pattern}'")?;
} else {
matching_remotes.extend(remotes);
}
}
match &unmatched_patterns[..] {
[] => {} // Everything matched, all good
[pattern] if pattern.is_exact() => {
return Err(user_error(format!("No git remote named '{pattern}'")))
}
patterns => {
return Err(user_error(format!(
"No matching git remotes for patterns: {}",
patterns.iter().join(", ")
)))
}
if matching_remotes.is_empty() {
return Err(user_error("No git remotes to push"));
}
let remotes = all_remotes
let remotes = matching_remotes
.iter()
.filter(|r| matching_remotes.contains(r))
.map(|r| r.as_ref())
.sorted()
.collect_vec();
#[cfg(feature = "git2")]

View File

@ -322,7 +322,8 @@ fn test_git_fetch_with_glob_with_no_matching_remotes(subprocess: bool) {
insta::allow_duplicates! {
insta::assert_snapshot!(output, @r"
------- stderr -------
Error: No matching git remotes for patterns: rem*
Warning: No git remotes matching 'rem*'
Error: No git remotes to push
[EOF]
[exit status: 1]
");
@ -386,6 +387,28 @@ fn test_git_fetch_multiple_remotes_from_config(subprocess: bool) {
}
}
#[cfg_attr(feature = "git2", test_case(false; "use git2 for remote calls"))]
#[test_case(true; "spawn a git subprocess for remote calls")]
fn test_git_fetch_no_matching_remote(subprocess: bool) {
let test_env = TestEnvironment::default().with_git_subprocess(subprocess);
test_env.run_jj_in(".", ["git", "init", "repo"]).success();
let work_dir = test_env.work_dir("repo");
let output = work_dir.run_jj(["git", "fetch", "--remote", "rem1"]);
insta::allow_duplicates! {
insta::assert_snapshot!(output, @r"
------- stderr -------
Warning: No git remotes matching 'rem1'
Error: No git remotes to push
[EOF]
[exit status: 1]
");
}
insta::allow_duplicates! {
insta::assert_snapshot!(get_bookmark_output(&work_dir), @"");
}
}
#[cfg_attr(feature = "git2", test_case(false; "use git2 for remote calls"))]
#[test_case(true; "spawn a git subprocess for remote calls")]
fn test_git_fetch_nonexistent_remote(subprocess: bool) {
@ -398,14 +421,16 @@ fn test_git_fetch_nonexistent_remote(subprocess: bool) {
insta::allow_duplicates! {
insta::assert_snapshot!(output, @r"
------- stderr -------
Error: No git remote named 'rem2'
Warning: No git remotes matching 'rem2'
bookmark: rem1@rem1 [new] untracked
[EOF]
[exit status: 1]
");
}
insta::allow_duplicates! {
// No remote should have been fetched as part of the failing transaction
insta::assert_snapshot!(get_bookmark_output(&work_dir), @"");
insta::assert_snapshot!(get_bookmark_output(&work_dir), @r"
rem1@rem1: ppspxspk 4acd0343 message
[EOF]
");
}
}
@ -422,14 +447,16 @@ fn test_git_fetch_nonexistent_remote_from_config(subprocess: bool) {
insta::allow_duplicates! {
insta::assert_snapshot!(output, @r"
------- stderr -------
Error: No git remote named 'rem2'
Warning: No git remotes matching 'rem2'
bookmark: rem1@rem1 [new] untracked
[EOF]
[exit status: 1]
");
}
// No remote should have been fetched as part of the failing transaction
insta::allow_duplicates! {
insta::assert_snapshot!(get_bookmark_output(&work_dir), @"");
insta::assert_snapshot!(get_bookmark_output(&work_dir), @r"
rem1@rem1: ppspxspk 4acd0343 message
[EOF]
");
}
}