diff --git a/CHANGELOG.md b/CHANGELOG.md index 593fb01494..9ed1e9bdc2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -45,6 +45,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#1252](https://github.com/martinvonz/jj/issues/1252), [#2971](https://github.com/martinvonz/jj/issues/2971)). This may become the default depending on feedback. +* `jj git fetch` now automatically prints new remote branches and tags by default. ### Fixed bugs diff --git a/cli/src/cli_util.rs b/cli/src/cli_util.rs index bcf4beeeed..6771d56c08 100644 --- a/cli/src/cli_util.rs +++ b/cli/src/cli_util.rs @@ -948,7 +948,7 @@ impl WorkspaceCommandHelper { return Ok(()); } - print_git_import_stats(ui, &stats)?; + print_git_import_stats(ui, &stats, false)?; let mut tx = tx.into_inner(); // Rebase here to show slightly different status message. let num_rebased = tx.mut_repo().rebase_descendants(&self.settings)?; diff --git a/cli/src/commands/git.rs b/cli/src/commands/git.rs index 8831721512..56e8a3a454 100644 --- a/cli/src/commands/git.rs +++ b/cli/src/commands/git.rs @@ -447,7 +447,7 @@ fn init_git_refs( if !tx.mut_repo().has_changes() { return Ok(repo); } - print_git_import_stats(ui, &stats)?; + print_git_import_stats(ui, &stats, false)?; if colocated { // If git.auto-local-branch = true, local branches could be created for // the imported remote branches. @@ -537,7 +537,7 @@ fn cmd_git_fetch( GitFetchError::InternalGitError(err) => map_git_error(err), _ => user_error(err), })?; - print_git_import_stats(ui, &stats.import_stats)?; + print_git_import_stats(ui, &stats.import_stats, true)?; } tx.finish( ui, @@ -751,7 +751,7 @@ fn do_git_clone( unreachable!("we didn't provide any globs") } })?; - print_git_import_stats(ui, &stats.import_stats)?; + print_git_import_stats(ui, &stats.import_stats, false)?; fetch_tx.finish(ui, "fetch from git remote into empty repo")?; Ok((workspace_command, stats)) } @@ -1190,7 +1190,7 @@ fn cmd_git_import( // That's why cmd_git_export() doesn't export the HEAD ref. git::import_head(tx.mut_repo())?; let stats = git::import_refs(tx.mut_repo(), &command.settings().git_settings())?; - print_git_import_stats(ui, &stats)?; + print_git_import_stats(ui, &stats, false)?; tx.finish(ui, "import git refs")?; Ok(()) } diff --git a/cli/src/git_util.rs b/cli/src/git_util.rs index df0b467e03..97b841826b 100644 --- a/cli/src/git_util.rs +++ b/cli/src/git_util.rs @@ -21,7 +21,7 @@ use std::sync::Mutex; use std::time::Instant; use std::{error, iter}; -use jj_lib::git::{self, FailedRefExport, FailedRefExportReason, GitImportStats}; +use jj_lib::git::{self, FailedRefExport, FailedRefExportReason, GitImportStats, RefName}; use jj_lib::git_backend::GitBackend; use jj_lib::repo::{ReadonlyRepo, Repo as _}; use jj_lib::store::Store; @@ -172,7 +172,45 @@ pub fn with_remote_git_callbacks( f(callbacks) } -pub fn print_git_import_stats(ui: &mut Ui, stats: &GitImportStats) -> Result<(), CommandError> { +pub fn print_git_import_stats( + ui: &mut Ui, + stats: &GitImportStats, + show_ref_stats: bool, +) -> Result<(), CommandError> { + use std::collections::BTreeMap; + + if show_ref_stats && !stats.new_remote_refs.is_empty() { + let mut new_remote_branches: BTreeMap<_, Vec<_>> = BTreeMap::new(); + let mut tags = Vec::new(); + let mut local_branches = Vec::new(); + for refname in &stats.new_remote_refs { + match refname { + RefName::RemoteBranch { branch, remote } => { + new_remote_branches.entry(remote).or_default().push(branch); + } + RefName::Tag(tag) => tags.push(tag), + RefName::LocalBranch(b) => local_branches.push(b), + } + } + local_branches.sort_unstable(); + for branch in local_branches { + writeln!(ui.stderr(), "* [imported local branch] {branch}")?; + } + for (remote, branches) in new_remote_branches.iter_mut() { + writeln!(ui.stderr(), "From {remote}")?; + branches.sort_unstable(); + for branch in branches { + writeln!(ui.stderr(), "* [new untracked branch] {branch}")?; + } + } + if !tags.is_empty() { + writeln!(ui.stderr(), "Tags:")?; + tags.sort_unstable(); + for tag in tags { + writeln!(ui.stderr(), "* [new tag] {tag}")?; + } + } + } if !stats.abandoned_commits.is_empty() { writeln!( ui.stderr(), diff --git a/cli/tests/test_branch_command.rs b/cli/tests/test_branch_command.rs index e957e6d667..ca0ce23828 100644 --- a/cli/tests/test_branch_command.rs +++ b/cli/tests/test_branch_command.rs @@ -536,7 +536,10 @@ fn test_branch_forget_fetched_branch() { // We can fetch feature1 again. let (stdout, stderr) = test_env.jj_cmd_ok(&repo_path, &["git", "fetch", "--remote=origin"]); insta::assert_snapshot!(stdout, @""); - insta::assert_snapshot!(stderr, @""); + insta::assert_snapshot!(stderr, @r###" + From origin + * [new untracked branch] feature1 + "###); insta::assert_snapshot!(get_branch_output(&test_env, &repo_path), @r###" feature1: mzyxwzks 9f01a0e0 message @origin: mzyxwzks 9f01a0e0 message @@ -548,7 +551,10 @@ fn test_branch_forget_fetched_branch() { // Fetch works even without the export-import let (stdout, stderr) = test_env.jj_cmd_ok(&repo_path, &["git", "fetch", "--remote=origin"]); insta::assert_snapshot!(stdout, @""); - insta::assert_snapshot!(stderr, @""); + insta::assert_snapshot!(stderr, @r###" + From origin + * [new untracked branch] feature1 + "###); insta::assert_snapshot!(get_branch_output(&test_env, &repo_path), @r###" feature1: mzyxwzks 9f01a0e0 message @origin: mzyxwzks 9f01a0e0 message @@ -574,7 +580,10 @@ fn test_branch_forget_fetched_branch() { // Fetching a moved branch does not create a conflict let (stdout, stderr) = test_env.jj_cmd_ok(&repo_path, &["git", "fetch", "--remote=origin"]); insta::assert_snapshot!(stdout, @""); - insta::assert_snapshot!(stderr, @""); + insta::assert_snapshot!(stderr, @r###" + From origin + * [new untracked branch] feature1 + "###); insta::assert_snapshot!(get_branch_output(&test_env, &repo_path), @r###" feature1: ooosovrs 38aefb17 (empty) another message @origin: ooosovrs 38aefb17 (empty) another message @@ -687,7 +696,12 @@ fn test_branch_track_untrack() { ); test_env.add_config("git.auto-local-branch = false"); let (_stdout, stderr) = test_env.jj_cmd_ok(&repo_path, &["git", "fetch"]); - insta::assert_snapshot!(stderr, @""); + insta::assert_snapshot!(stderr, @r###" + From origin + * [new untracked branch] feature1 + * [new untracked branch] feature2 + * [new untracked branch] main + "###); insta::assert_snapshot!(get_branch_output(&test_env, &repo_path), @r###" feature1@origin: sptzoqmo 7b33f629 commit 1 feature2@origin: sptzoqmo 7b33f629 commit 1 @@ -759,7 +773,12 @@ fn test_branch_track_untrack() { ], ); let (_stdout, stderr) = test_env.jj_cmd_ok(&repo_path, &["git", "fetch"]); - insta::assert_snapshot!(stderr, @""); + insta::assert_snapshot!(stderr, @r###" + From origin + * [new untracked branch] feature1 + * [new untracked branch] feature2 + * [new untracked branch] main + "###); insta::assert_snapshot!(get_branch_output(&test_env, &repo_path), @r###" feature1: sptzoqmo 7b33f629 commit 1 feature1@origin: mmqqkyyt 40dabdaf commit 2 @@ -791,6 +810,11 @@ fn test_branch_track_untrack() { test_env.add_config("git.auto-local-branch = true"); let (_stdout, stderr) = test_env.jj_cmd_ok(&repo_path, &["git", "fetch"]); insta::assert_snapshot!(stderr, @r###" + From origin + * [new untracked branch] feature1 + * [new untracked branch] feature2 + * [new untracked branch] feature3 + * [new untracked branch] main Abandoned 1 commits that are no longer reachable. "###); insta::assert_snapshot!(get_branch_output(&test_env, &repo_path), @r###" @@ -847,7 +871,11 @@ fn test_branch_track_untrack_patterns() { // Fetch new commit without auto tracking test_env.add_config("git.auto-local-branch = false"); let (_stdout, stderr) = test_env.jj_cmd_ok(&repo_path, &["git", "fetch"]); - insta::assert_snapshot!(stderr, @""); + insta::assert_snapshot!(stderr, @r###" + From origin + * [new untracked branch] feature1 + * [new untracked branch] feature2 + "###); // Track local branch test_env.jj_cmd_ok(&repo_path, &["branch", "create", "main"]); diff --git a/cli/tests/test_git_colocated.rs b/cli/tests/test_git_colocated.rs index d59bbf459c..ef99687536 100644 --- a/cli/tests/test_git_colocated.rs +++ b/cli/tests/test_git_colocated.rs @@ -507,6 +507,9 @@ fn test_git_colocated_fetch_deleted_or_moved_branch() { let (stdout, stderr) = test_env.jj_cmd_ok(&clone_path, &["git", "fetch"]); insta::assert_snapshot!(stdout, @""); insta::assert_snapshot!(stderr, @r###" + From origin + * [new untracked branch] B_to_delete + * [new untracked branch] C_to_move Abandoned 2 commits that are no longer reachable. "###); // "original C" and "B_to_delete" are abandoned, as the corresponding branches diff --git a/cli/tests/test_git_fetch.rs b/cli/tests/test_git_fetch.rs index 7b45f8faf1..6356f2fd30 100644 --- a/cli/tests/test_git_fetch.rs +++ b/cli/tests/test_git_fetch.rs @@ -111,7 +111,10 @@ fn test_git_fetch_single_remote() { .jj_cmd(&repo_path, &["git", "fetch"]) .assert() .success() - .stderr("Fetching from the only existing remote: rem1\n"); + .stderr( + "Fetching from the only existing remote: rem1\nFrom rem1\n* [new untracked branch] \ + rem1\n", + ); insta::assert_snapshot!(get_branch_output(&test_env, &repo_path), @r###" rem1: qxosxrvv 6a211027 message @rem1: qxosxrvv 6a211027 message @@ -237,6 +240,8 @@ fn test_git_fetch_nonexistent_remote() { &["git", "fetch", "--remote", "rem1", "--remote", "rem2"], ); insta::assert_snapshot!(stderr, @r###" + From rem1 + * [new untracked branch] rem1 Error: No git remote named 'rem2' "###); // No remote should have been fetched as part of the failing transaction @@ -253,6 +258,8 @@ fn test_git_fetch_nonexistent_remote_from_config() { let stderr = &test_env.jj_cmd_failure(&repo_path, &["git", "fetch"]); insta::assert_snapshot!(stderr, @r###" + From rem1 + * [new untracked branch] rem1 Error: No git remote named 'rem2' "###); // No remote should have been fetched as part of the failing transaction @@ -461,7 +468,13 @@ fn test_git_fetch_all() { insta::assert_snapshot!(get_branch_output(&test_env, &target_jj_repo_path), @""); let (stdout, stderr) = test_env.jj_cmd_ok(&target_jj_repo_path, &["git", "fetch"]); insta::assert_snapshot!(stdout, @""); - insta::assert_snapshot!(stderr, @""); + insta::assert_snapshot!(stderr, @r###" + From origin + * [new untracked branch] a1 + * [new untracked branch] a2 + * [new untracked branch] b + * [new untracked branch] trunk1 + "###); insta::assert_snapshot!(get_branch_output(&test_env, &target_jj_repo_path), @r###" a1: nknoxmzm 359a9a02 descr_for_a1 @origin: nknoxmzm 359a9a02 descr_for_a1 @@ -529,6 +542,11 @@ fn test_git_fetch_all() { let (stdout, stderr) = test_env.jj_cmd_ok(&target_jj_repo_path, &["git", "fetch"]); insta::assert_snapshot!(stdout, @""); insta::assert_snapshot!(stderr, @r###" + From origin + * [new untracked branch] a1 + * [new untracked branch] a2 + * [new untracked branch] b + * [new untracked branch] trunk2 Abandoned 2 commits that are no longer reachable. "###); insta::assert_snapshot!(get_branch_output(&test_env, &target_jj_repo_path), @r###" @@ -616,7 +634,10 @@ fn test_git_fetch_some_of_many_branches() { let (stdout, stderr) = test_env.jj_cmd_ok(&target_jj_repo_path, &["git", "fetch", "--branch", "b"]); insta::assert_snapshot!(stdout, @""); - insta::assert_snapshot!(stderr, @""); + insta::assert_snapshot!(stderr, @r###" + From origin + * [new untracked branch] b + "###); insta::assert_snapshot!(get_log_output(&test_env, &target_jj_repo_path), @r###" ◉ c7d4bdcbc215 descr_for_b b ◉ ff36dc55760e descr_for_trunk1 @@ -635,7 +656,11 @@ fn test_git_fetch_some_of_many_branches() { &["git", "fetch", "--branch", "glob:a*"], ); insta::assert_snapshot!(stdout, @""); - insta::assert_snapshot!(stderr, @""); + insta::assert_snapshot!(stderr, @r###" + From origin + * [new untracked branch] a1 + * [new untracked branch] a2 + "###); insta::assert_snapshot!(get_log_output(&test_env, &target_jj_repo_path), @r###" ◉ decaa3966c83 descr_for_a2 a2 │ ◉ 359a9a02457d descr_for_a1 a1 @@ -704,6 +729,9 @@ fn test_git_fetch_some_of_many_branches() { ); insta::assert_snapshot!(stdout, @""); insta::assert_snapshot!(stderr, @r###" + From origin + * [new untracked branch] a1 + * [new untracked branch] b Abandoned 1 commits that are no longer reachable. "###); insta::assert_snapshot!(get_log_output(&test_env, &target_jj_repo_path), @r###" @@ -741,6 +769,8 @@ fn test_git_fetch_some_of_many_branches() { ); insta::assert_snapshot!(stdout, @""); insta::assert_snapshot!(stderr, @r###" + From origin + * [new untracked branch] a2 Abandoned 1 commits that are no longer reachable. "###); insta::assert_snapshot!(get_log_output(&test_env, &target_jj_repo_path), @r###" @@ -808,7 +838,11 @@ fn test_git_fetch_undo() { &["git", "fetch", "--branch", "b", "--branch", "a1"], ); insta::assert_snapshot!(stdout, @""); - insta::assert_snapshot!(stderr, @""); + insta::assert_snapshot!(stderr, @r###" + From origin + * [new untracked branch] a1 + * [new untracked branch] b + "###); insta::assert_snapshot!(get_log_output(&test_env, &target_jj_repo_path), @r###" ◉ c7d4bdcbc215 descr_for_b b │ ◉ 359a9a02457d descr_for_a1 a1 @@ -830,7 +864,10 @@ fn test_git_fetch_undo() { let (stdout, stderr) = test_env.jj_cmd_ok(&target_jj_repo_path, &["git", "fetch", "--branch", "b"]); insta::assert_snapshot!(stdout, @""); - insta::assert_snapshot!(stderr, @""); + insta::assert_snapshot!(stderr, @r###" + From origin + * [new untracked branch] b + "###); insta::assert_snapshot!(get_log_output(&test_env, &target_jj_repo_path), @r###" ◉ c7d4bdcbc215 descr_for_b b ◉ ff36dc55760e descr_for_trunk1 @@ -880,7 +917,10 @@ fn test_fetch_undo_what() { // Fetch a branch let (stdout, stderr) = test_env.jj_cmd_ok(&repo_path, &["git", "fetch", "--branch", "b"]); insta::assert_snapshot!(stdout, @""); - insta::assert_snapshot!(stderr, @""); + insta::assert_snapshot!(stderr, @r###" + From origin + * [new untracked branch] b + "###); insta::assert_snapshot!(get_log_output(&test_env, &repo_path), @r###" ◉ c7d4bdcbc215 descr_for_b b ◉ ff36dc55760e descr_for_trunk1 @@ -967,7 +1007,10 @@ fn test_git_fetch_remove_fetch() { // Check that origin@origin is properly recreated let (stdout, stderr) = test_env.jj_cmd_ok(&repo_path, &["git", "fetch"]); insta::assert_snapshot!(stdout, @""); - insta::assert_snapshot!(stderr, @""); + insta::assert_snapshot!(stderr, @r###" + From origin + * [new untracked branch] origin + "###); insta::assert_snapshot!(get_branch_output(&test_env, &repo_path), @r###" origin (conflicted): + qpvuntsm 230dd059 (empty) (no description set) @@ -1050,7 +1093,13 @@ fn test_git_fetch_removed_branch() { // Fetch all branches let (stdout, stderr) = test_env.jj_cmd_ok(&target_jj_repo_path, &["git", "fetch"]); insta::assert_snapshot!(stdout, @""); - insta::assert_snapshot!(stderr, @""); + insta::assert_snapshot!(stderr, @r###" + From origin + * [new untracked branch] a1 + * [new untracked branch] a2 + * [new untracked branch] b + * [new untracked branch] trunk1 + "###); insta::assert_snapshot!(get_log_output(&test_env, &target_jj_repo_path), @r###" ◉ c7d4bdcbc215 descr_for_b b │ ◉ decaa3966c83 descr_for_a2 a2 @@ -1090,6 +1139,8 @@ fn test_git_fetch_removed_branch() { test_env.jj_cmd_ok(&target_jj_repo_path, &["git", "fetch", "--branch", "a2"]); insta::assert_snapshot!(stdout, @""); insta::assert_snapshot!(stderr, @r###" + From origin + * [new untracked branch] a2 Abandoned 1 commits that are no longer reachable. "###); insta::assert_snapshot!(get_log_output(&test_env, &target_jj_repo_path), @r###" @@ -1136,7 +1187,13 @@ fn test_git_fetch_removed_parent_branch() { // Fetch all branches let (stdout, stderr) = test_env.jj_cmd_ok(&target_jj_repo_path, &["git", "fetch"]); insta::assert_snapshot!(stdout, @""); - insta::assert_snapshot!(stderr, @""); + insta::assert_snapshot!(stderr, @r###" + From origin + * [new untracked branch] a1 + * [new untracked branch] a2 + * [new untracked branch] b + * [new untracked branch] trunk1 + "###); insta::assert_snapshot!(get_log_output(&test_env, &target_jj_repo_path), @r###" ◉ c7d4bdcbc215 descr_for_b b │ ◉ decaa3966c83 descr_for_a2 a2 @@ -1163,6 +1220,9 @@ fn test_git_fetch_removed_parent_branch() { ); insta::assert_snapshot!(stdout, @""); insta::assert_snapshot!(stderr, @r###" + From origin + * [new untracked branch] a1 + * [new untracked branch] trunk1 Abandoned 1 commits that are no longer reachable. "###); insta::assert_snapshot!(get_log_output(&test_env, &target_jj_repo_path), @r###" diff --git a/lib/src/git.rs b/lib/src/git.rs index d38636731d..f9706e8f73 100644 --- a/lib/src/git.rs +++ b/lib/src/git.rs @@ -15,7 +15,7 @@ #![allow(missing_docs)] use std::borrow::Cow; -use std::collections::{BTreeMap, HashMap, HashSet}; +use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet}; use std::default::Default; use std::io::Read; use std::path::PathBuf; @@ -201,10 +201,12 @@ impl GitImportError { } /// Describes changes made by `import_refs()` or `fetch()`. -#[derive(Clone, Debug, Eq, PartialEq)] +#[derive(Clone, Debug, Eq, PartialEq, Default)] pub struct GitImportStats { /// Commits superseded by newly imported commits. pub abandoned_commits: Vec, + /// Newly fetched remote refs. + pub new_remote_refs: BTreeSet, } #[derive(Debug)] @@ -326,7 +328,10 @@ pub fn import_some_refs( } else { vec![] }; - let stats = GitImportStats { abandoned_commits }; + let stats = GitImportStats { + abandoned_commits, + new_remote_refs: BTreeSet::from_iter(changed_remote_refs.keys().cloned()), + }; Ok(stats) } @@ -1089,7 +1094,7 @@ pub enum GitFetchError { } /// Describes successful `fetch()` result. -#[derive(Clone, Debug, Eq, PartialEq)] +#[derive(Clone, Debug, Eq, PartialEq, Default)] pub struct GitFetchStats { /// Remote's default branch. pub default_branch: Option, @@ -1135,12 +1140,7 @@ pub fn fetch( .ok_or(GitFetchError::InvalidBranchPattern)?; if refspecs.is_empty() { // Don't fall back to the base refspecs. - let stats = GitFetchStats { - default_branch: None, - import_stats: GitImportStats { - abandoned_commits: vec![], - }, - }; + let stats = GitFetchStats::default(); return Ok(stats); } tracing::debug!("remote.download");