diff --git a/CHANGELOG.md b/CHANGELOG.md index e71a32f05e3..eeffb78add6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,6 +33,9 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). back to prompting the user if the heuristics are inconclusive. It can also run in non-interactive mode, which aborts if prompting would be needed. +* `jj git fetch`'s output can be customized with `templates.git_fetch` or `-T` + [#9311](https://github.com/jj-vcs/jj/pull/9311). + ### Fixed bugs * `jj arrange` now scrolls the viewport to keep the selected commit visible diff --git a/cli/src/cli_util.rs b/cli/src/cli_util.rs index d716ef7a108..401db2f7cab 100644 --- a/cli/src/cli_util.rs +++ b/cli/src/cli_util.rs @@ -161,7 +161,6 @@ use crate::command_error::config_error_with_message; use crate::command_error::handle_command_result; use crate::command_error::internal_error; use crate::command_error::internal_error_with_message; -use crate::command_error::print_error_sources; use crate::command_error::print_parse_diagnostics; use crate::command_error::user_error; use crate::command_error::user_error_with_message; @@ -1237,6 +1236,8 @@ pub struct WorkspaceCommandHelper { // TODO: Parsed template can be cached if it doesn't capture 'repo lifetime commit_summary_template_text: String, op_summary_template_text: String, + #[cfg(feature = "git")] + fetch_summary_template_text: String, may_snapshot_working_copy: bool, may_update_working_copy: bool, } @@ -1274,6 +1275,8 @@ impl WorkspaceCommandHelper { let settings = workspace.settings(); let commit_summary_template_text = settings.get_string("templates.commit_summary")?; let op_summary_template_text = settings.get_string("templates.op_summary")?; + #[cfg(feature = "git")] + let fetch_summary_template_text = settings.get_string("templates.git_fetch")?; let may_update_working_copy = may_snapshot_working_copy && env.command.should_commit_transaction(); @@ -1282,6 +1285,8 @@ impl WorkspaceCommandHelper { user_repo: ReadonlyUserRepo::new(repo), env, commit_summary_template_text, + #[cfg(feature = "git")] + fetch_summary_template_text, op_summary_template_text, may_snapshot_working_copy, may_update_working_copy, @@ -1290,6 +1295,8 @@ impl WorkspaceCommandHelper { // mutable operation. helper.parse_operation_template(ui, &helper.op_summary_template_text)?; helper.parse_commit_template(ui, &helper.commit_summary_template_text)?; + #[cfg(feature = "git")] + helper.parse_refstatus_template(ui, &helper.fetch_summary_template_text)?; helper.parse_commit_template(ui, SHORT_CHANGE_ID_TEMPLATE_TEXT)?; Ok(helper) } @@ -1511,6 +1518,11 @@ impl WorkspaceCommandHelper { &self.env } + #[cfg(feature = "git")] + pub fn fetch_summary_template_text(&self) -> &str { + &self.fetch_summary_template_text + } + async fn prepare_working_copy_mutation(&self) -> Result { self.check_working_copy_writable()?; if let Some(wc_commit_id) = self.get_wc_commit_id() { @@ -1931,7 +1943,7 @@ to the current parents may contain changes from multiple commits. } /// Parses template that is validated by `Self::new()`. - fn reparse_valid_template<'a, C, L>( + pub(crate) fn reparse_valid_template<'a, C, L>( &self, language: &L, template_text: &str, @@ -1960,6 +1972,17 @@ to the current parents may contain changes from multiple commits. self.parse_template(ui, &language, template_text) } + /// Parses refstatus template into evaluation tree. + #[cfg(feature = "git")] + pub fn parse_refstatus_template( + &self, + ui: &Ui, + template_text: &str, + ) -> Result, CommandError> { + let language = self.commit_template_language(); + self.parse_template(ui, &language, template_text) + } + /// Parses commit template into evaluation tree. pub fn parse_operation_template( &self, @@ -2741,7 +2764,7 @@ async fn try_reset_git_head( Ok(()) => Ok(()), Err(err @ jj_lib::git::GitResetHeadError::UpdateHeadRef(_)) => { writeln!(ui.warning_default(), "{err}")?; - print_error_sources(ui, err.source())?; + crate::command_error::print_error_sources(ui, err.source())?; Ok(()) } Err(err) => Err(err.into()), diff --git a/cli/src/commands/git/clone.rs b/cli/src/commands/git/clone.rs index ee02f498bed..596cf0d6504 100644 --- a/cli/src/commands/git/clone.rs +++ b/cli/src/commands/git/clone.rs @@ -453,7 +453,10 @@ async fn fetch_new_remote( let remote_symbol = name.to_remote_symbol(remote_name); tx.repo_mut().track_remote_bookmark(remote_symbol).await?; } - print_git_import_stats(ui, &tx, &import_stats)?; + { + let template = crate::git_util::commit_fetch_template(&tx); + print_git_import_stats(ui, &tx, &import_stats, &template)?; + } tx.finish(ui, "fetch from git remote into empty repo") .await?; Ok((working_branch.map(ToOwned::to_owned), working_is_default)) diff --git a/cli/src/commands/git/fetch.rs b/cli/src/commands/git/fetch.rs index b62f2a3ad90..353ac26e118 100644 --- a/cli/src/commands/git/fetch.rs +++ b/cli/src/commands/git/fetch.rs @@ -245,7 +245,10 @@ pub async fn cmd_git_fetch( } let import_stats = git_fetch.import_refs().await?; - print_git_import_stats(ui, &tx, &import_stats)?; + { + let template = crate::git_util::commit_fetch_template(&tx); + print_git_import_stats(ui, &tx, &import_stats, &template)?; + } if let Some(bookmark_expr) = &common_bookmark_expr { warn_if_branches_not_found(ui, &tx, bookmark_expr, &matching_remotes)?; diff --git a/cli/src/commands/git/import.rs b/cli/src/commands/git/import.rs index c46d6a9c1c5..af9d4d8e9b4 100644 --- a/cli/src/commands/git/import.rs +++ b/cli/src/commands/git/import.rs @@ -54,7 +54,10 @@ pub async fn cmd_git_import( let import_options = load_git_import_options(ui, &git_settings, &remote_settings)?; let mut tx = workspace_command.start_transaction(); let stats = git::import_refs(tx.repo_mut(), &import_options).await?; - print_git_import_stats(ui, &tx, &stats)?; + { + let template = crate::git_util::commit_fetch_template(&tx); + print_git_import_stats(ui, &tx, &stats, &template)?; + } tx.finish(ui, "import git refs").await?; Ok(()) } diff --git a/cli/src/commit_templater.rs b/cli/src/commit_templater.rs index eec1dc5c480..859d0a574da 100644 --- a/cli/src/commit_templater.rs +++ b/cli/src/commit_templater.rs @@ -97,6 +97,8 @@ use crate::diff_util::DiffStatEntry; use crate::diff_util::DiffStatOptions; use crate::diff_util::DiffStats; use crate::formatter::Formatter; +#[cfg(feature = "git")] +use crate::git_util::RefStatus; use crate::operation_templater; use crate::operation_templater::OperationTemplateBuildFnTable; use crate::operation_templater::OperationTemplateEnvironment; @@ -407,6 +409,12 @@ impl<'repo> TemplateLanguage<'repo> for CommitTemplateLanguage<'repo> { let build = template_parser::lookup_method(type_name, table, function)?; build(self, diagnostics, build_ctx, property, function) } + #[cfg(feature = "git")] + CommitTemplatePropertyKind::RefStatus(property) => { + let table = &self.build_fn_table.ref_status_methods; + let build = template_parser::lookup_method(type_name, table, function)?; + build(self, diagnostics, build_ctx, property, function) + } } } } @@ -474,6 +482,8 @@ pub enum CommitTemplatePropertyKind<'repo> { AnnotationLine(BoxedTemplateProperty<'repo, AnnotationLine>), Trailer(BoxedTemplateProperty<'repo, Trailer>), TrailerList(BoxedTemplateProperty<'repo, Vec>), + #[cfg(feature = "git")] + RefStatus(BoxedTemplateProperty<'repo, RefStatus>), } template_builder::impl_core_property_wrappers!(<'repo> CommitTemplatePropertyKind<'repo> => Core); @@ -508,6 +518,8 @@ template_builder::impl_property_wrappers!(<'repo> CommitTemplatePropertyKind<'re AnnotationLine(AnnotationLine), Trailer(Trailer), TrailerList(Vec), + #[cfg(feature = "git")] + RefStatus(RefStatus), }); impl<'repo> CoreTemplatePropertyVar<'repo> for CommitTemplatePropertyKind<'repo> { @@ -556,6 +568,8 @@ impl<'repo> CoreTemplatePropertyVar<'repo> for CommitTemplatePropertyKind<'repo> Self::AnnotationLine(_) => "AnnotationLine", Self::Trailer(_) => "Trailer", Self::TrailerList(_) => "List", + #[cfg(feature = "git")] + Self::RefStatus(_) => "RefStatus", } } @@ -616,6 +630,8 @@ impl<'repo> CoreTemplatePropertyVar<'repo> for CommitTemplatePropertyKind<'repo> Self::AnnotationLine(_) => Err(self), Self::Trailer(_) => Err(self), Self::TrailerList(property) => Ok(property.map(|l| !l.is_empty()).into_dyn()), + #[cfg(feature = "git")] + Self::RefStatus(_) => Err(self), } } @@ -668,6 +684,8 @@ impl<'repo> CoreTemplatePropertyVar<'repo> for CommitTemplatePropertyKind<'repo> Self::AnnotationLine(_) => None, Self::Trailer(_) => None, Self::TrailerList(_) => None, + #[cfg(feature = "git")] + Self::RefStatus(_) => None, } } @@ -704,6 +722,8 @@ impl<'repo> CoreTemplatePropertyVar<'repo> for CommitTemplatePropertyKind<'repo> Self::AnnotationLine(_) => None, Self::Trailer(property) => Some(property.into_template()), Self::TrailerList(property) => Some(property.into_template()), + #[cfg(feature = "git")] + Self::RefStatus(property) => Some(property.into_template()), } } @@ -773,6 +793,8 @@ impl<'repo> CoreTemplatePropertyVar<'repo> for CommitTemplatePropertyKind<'repo> (Self::AnnotationLine(_), _) => None, (Self::Trailer(_), _) => None, (Self::TrailerList(_), _) => None, + #[cfg(feature = "git")] + (Self::RefStatus(_), _) => None, } } @@ -815,6 +837,8 @@ impl<'repo> CoreTemplatePropertyVar<'repo> for CommitTemplatePropertyKind<'repo> (Self::AnnotationLine(_), _) => None, (Self::Trailer(_), _) => None, (Self::TrailerList(_), _) => None, + #[cfg(feature = "git")] + (Self::RefStatus(_), _) => None, } } } @@ -853,6 +877,8 @@ pub struct CommitTemplateBuildFnTable<'repo> { pub annotation_line_methods: CommitTemplateBuildMethodFnMap<'repo, AnnotationLine>, pub trailer_methods: CommitTemplateBuildMethodFnMap<'repo, Trailer>, pub trailer_list_methods: CommitTemplateBuildMethodFnMap<'repo, Vec>, + #[cfg(feature = "git")] + pub ref_status_methods: CommitTemplateBuildMethodFnMap<'repo, RefStatus>, } impl CommitTemplateBuildFnTable<'_> { @@ -883,6 +909,8 @@ impl CommitTemplateBuildFnTable<'_> { annotation_line_methods: HashMap::new(), trailer_methods: HashMap::new(), trailer_list_methods: HashMap::new(), + #[cfg(feature = "git")] + ref_status_methods: HashMap::new(), } } @@ -913,6 +941,8 @@ impl CommitTemplateBuildFnTable<'_> { annotation_line_methods, trailer_methods, trailer_list_methods, + #[cfg(feature = "git")] + ref_status_methods, } = other; self.core.merge(core); @@ -958,6 +988,8 @@ impl CommitTemplateBuildFnTable<'_> { merge_fn_map(&mut self.annotation_line_methods, annotation_line_methods); merge_fn_map(&mut self.trailer_methods, trailer_methods); merge_fn_map(&mut self.trailer_list_methods, trailer_list_methods); + #[cfg(feature = "git")] + merge_fn_map(&mut self.ref_status_methods, ref_status_methods); } /// Creates new symbol table containing the builtin methods. @@ -990,6 +1022,8 @@ impl CommitTemplateBuildFnTable<'_> { annotation_line_methods: builtin_annotation_line_methods(), trailer_methods: builtin_trailer_methods(), trailer_list_methods: builtin_trailer_list_methods(), + #[cfg(feature = "git")] + ref_status_methods: builtin_ref_status_methods(), } } } @@ -2060,6 +2094,69 @@ impl Display for RefSymbolBuf { } } +#[cfg(feature = "git")] +impl Template for RefStatus { + fn format(&self, formatter: &mut TemplateFormatter) -> io::Result<()> { + write!(formatter, "{}", self.name()) + } +} + +#[cfg(feature = "git")] +fn builtin_ref_status_methods<'repo>() -> CommitTemplateBuildMethodFnMap<'repo, RefStatus> { + let mut map = CommitTemplateBuildMethodFnMap::::new(); + map.insert( + "name", + |_language, _diagnostics, _build_ctx, self_property, function| { + function.expect_no_arguments()?; + let out_property = self_property.map(|ref_status| ref_status.name().to_owned()); + Ok(out_property.into_dyn_wrapped()) + }, + ); + map.insert( + "tracked", + |_language, _diagnostics, _build_ctx, self_property, function| { + function.expect_no_arguments()?; + let out_property = self_property.map(|ref_status| ref_status.is_tracked()); + Ok(out_property.into_dyn_wrapped()) + }, + ); + map.insert( + "remote_ref_state", + |_language, _diagnostics, _build_ctx, self_property, function| { + function.expect_no_arguments()?; + let out_property = + self_property.map(|ref_status| ref_status.remote_ref_state().to_owned()); + Ok(out_property.into_dyn_wrapped()) + }, + ); + map.insert( + "import_status", + |_language, _diagnostics, _build_ctx, self_property, function| { + function.expect_no_arguments()?; + let out_property = + self_property.map(|ref_status| ref_status.import_status().to_owned()); + Ok(out_property.into_dyn_wrapped()) + }, + ); + map.insert( + "kind", + |_language, _diagnostics, _build_ctx, self_property, function| { + function.expect_no_arguments()?; + let out_property = self_property.map(|ref_status| ref_status.kind().to_owned()); + Ok(out_property.into_dyn_wrapped()) + }, + ); + map.insert( + "max_name_width", + |_language, _diagnostics, _build_ctx, self_property, function| { + function.expect_no_arguments()?; + let out_property = self_property.map(|ref_status| ref_status.max_name_width() as i64); + Ok(out_property.into_dyn_wrapped()) + }, + ); + map +} + impl Template for RefSymbolBuf { fn format(&self, formatter: &mut TemplateFormatter) -> io::Result<()> { write!(formatter, "{self}") diff --git a/cli/src/config-schema.json b/cli/src/config-schema.json index aa841243910..2f4de9b18c3 100644 --- a/cli/src/config-schema.json +++ b/cli/src/config-schema.json @@ -1106,6 +1106,10 @@ "type": "string", "description": "`jj file show`'s output" }, + "git_fetch": { + "type": "string", + "description": "`jj git fetch`'s output" + }, "git_push_bookmark": { "type": "string", "description": "Bookmark name to be assigned when pushing a change to Git remote" diff --git a/cli/src/config/templates.toml b/cli/src/config/templates.toml index afdb50828cf..f0a03d81803 100644 --- a/cli/src/config/templates.toml +++ b/cli/src/config/templates.toml @@ -27,6 +27,14 @@ evolog = 'builtin_evolog_compact' file_list = 'format_path(path) ++ "\n"' file_show = '' +git_fetch = ''' +separate(" ", + kind ++ ":", + pad_end(max_name_width, label(kind, name)), + "[" ++ import_status ++ "]", + remote_ref_state, +) ++ "\n" +''' git_push_bookmark = '"push-" ++ change_id.short()' log = 'builtin_log_compact' diff --git a/cli/src/git_util.rs b/cli/src/git_util.rs index d209a72d9ce..c172e79d858 100644 --- a/cli/src/git_util.rs +++ b/cli/src/git_util.rs @@ -57,6 +57,7 @@ use crate::command_error::user_error; use crate::formatter::Formatter; use crate::formatter::FormatterExt as _; use crate::revset_util::parse_remote_auto_track_bookmarks_map; +use crate::templater::TemplateRenderer; use crate::ui::ProgressOutput; use crate::ui::Ui; @@ -213,9 +214,10 @@ pub fn print_git_import_stats( ui: &Ui, tx: &WorkspaceCommandTransaction<'_>, stats: &GitImportStats, + ref_status_template: &TemplateRenderer<'_, RefStatus>, ) -> Result<(), CommandError> { if let Some(mut formatter) = ui.status_formatter() { - print_imported_changes(formatter.as_mut(), tx, stats)?; + print_imported_changes(formatter.as_mut(), tx, stats, ref_status_template)?; } print_failed_git_import(ui, stats)?; Ok(()) @@ -225,20 +227,27 @@ fn print_imported_changes( formatter: &mut dyn Formatter, tx: &WorkspaceCommandTransaction<'_>, stats: &GitImportStats, + ref_status_template: &TemplateRenderer<'_, RefStatus>, ) -> Result<(), CommandError> { for (kind, changes) in [ (GitRefKind::Bookmark, &stats.changed_remote_bookmarks), (GitRefKind::Tag, &stats.changed_remote_tags), ] { - let refs_stats = changes + let Some(max_name_width) = changes .iter() - .map(|update| RefStatus::new(kind, update, tx.repo())) - .collect_vec(); - let Some(max_width) = refs_stats.iter().map(|x| x.symbol.width()).max() else { + .map(|update| update.symbol.to_string().width()) + .max() + else { continue; }; + + let refs_stats = changes + .iter() + .map(|update| RefStatus::new(kind, update, tx.repo(), max_name_width)) + .collect_vec(); + for status in refs_stats { - status.output(max_width, formatter)?; + ref_status_template.format(&status, formatter)?; } } @@ -312,6 +321,19 @@ pub fn print_git_import_stats_summary(ui: &Ui, stats: &GitImportStats) -> Result Ok(()) } +/// Template for one-line summary of a fetched ref. +pub fn commit_fetch_template<'a>( + tx: &'a WorkspaceCommandTransaction +) -> TemplateRenderer<'a, RefStatus> { + let language = tx.commit_template_language(); + let helper = tx.base_workspace_helper(); + let template_text = helper.fetch_summary_template_text(); + + helper + .reparse_valid_template(&language, template_text) + .labeled(["ref_status"]) +} + pub struct Progress { next_print: Instant, buffer: String, @@ -396,15 +418,22 @@ fn draw_progress(progress: f32, buffer: &mut String, width: usize) { } } -struct RefStatus { +#[derive(Clone)] +pub struct RefStatus { ref_kind: GitRefKind, symbol: String, remote_ref_state: RemoteRefState, import_status: ImportStatus, + max_name_width: usize, } impl RefStatus { - fn new(ref_kind: GitRefKind, update: &GitImportRefUpdate, repo: &dyn Repo) -> Self { + fn new( + ref_kind: GitRefKind, + update: &GitImportRefUpdate, + repo: &dyn Repo, + max_name_width: usize, + ) -> Self { let new_remote_ref = match ref_kind { GitRefKind::Bookmark => repo.view().get_remote_bookmark(update.symbol.as_ref()), GitRefKind::Tag => repo.view().get_remote_tag(update.symbol.as_ref()), @@ -424,36 +453,46 @@ impl RefStatus { remote_ref_state: new_remote_ref.state, import_status, ref_kind, + max_name_width, } } - fn output(&self, max_symbol_width: usize, out: &mut dyn Formatter) -> std::io::Result<()> { - let tracking_status = match self.remote_ref_state { + pub fn name(&self) -> &str { + &self.symbol + } + + pub fn is_tracked(&self) -> bool { + matches!(self.remote_ref_state, RemoteRefState::Tracked) + } + + pub fn remote_ref_state(&self) -> &'static str { + match self.remote_ref_state { RemoteRefState::New => "untracked", RemoteRefState::Tracked => "tracked", - }; + } + } - let import_status = match self.import_status { + pub fn import_status(&self) -> &'static str { + match self.import_status { ImportStatus::New => "new", ImportStatus::Deleted => "deleted", ImportStatus::Updated => "updated", - }; - - let symbol_width = self.symbol.width(); - let pad_width = max_symbol_width.saturating_sub(symbol_width); - let padded_symbol = format!("{}{:>pad_width$}", self.symbol, "", pad_width = pad_width); + } + } - let label = match self.ref_kind { + pub fn kind(&self) -> &'static str { + match self.ref_kind { GitRefKind::Bookmark => "bookmark", GitRefKind::Tag => "tag", - }; + } + } - write!(out, "{label}: ")?; - write!(out.labeled(label), "{padded_symbol}")?; - writeln!(out, " [{import_status}] {tracking_status}") + pub fn max_name_width(&self) -> usize { + self.max_name_width } } +#[derive(Clone)] enum ImportStatus { New, Deleted, diff --git a/cli/src/template_builder.rs b/cli/src/template_builder.rs index 557a730c8e5..742b9924b32 100644 --- a/cli/src/template_builder.rs +++ b/cli/src/template_builder.rs @@ -143,24 +143,25 @@ macro_rules! impl_property_wrappers { macro_rules! _impl_property_wrappers_many { // lifetime/type parameters are packed in order to disable zipping. // https://github.com/rust-lang/rust/issues/96184#issuecomment-1294999418 - ($ps:tt, $a:lifetime, $kind:path, { $( $var:ident($ty:ty), )* }) => { + ($ps:tt, $a:lifetime, $kind:path, { $( $(#[$attr:meta])* $var:ident($ty:ty), )* }) => { $( $crate::template_builder::_impl_property_wrappers_one!( - $ps, $a, $kind, $var, $ty, std::convert::identity); + $ps, $a, $kind, [$(#[$attr])*], $var, $ty, std::convert::identity); )* }; // variant part in body is ignored so the same body can be reused for // implementing forwarding conversion. - ($ps:tt, $a:lifetime, $kind:path => $var:ident, { $( $ignored_var:ident($ty:ty), )* }) => { + ($ps:tt, $a:lifetime, $kind:path => $var:ident, { $( $(#[$attr:meta])* $ignored_var:ident($ty:ty), )* }) => { $( $crate::template_builder::_impl_property_wrappers_one!( - $ps, $a, $kind, $var, $ty, $crate::templater::WrapTemplateProperty::wrap_property); + $ps, $a, $kind, [$(#[$attr])*], $var, $ty, $crate::templater::WrapTemplateProperty::wrap_property); )* }; } macro_rules! _impl_property_wrappers_one { - ([$($p:tt)*], $a:lifetime, $kind:path, $var:ident, $ty:ty, $inner:path) => { + ([$($p:tt)*], $a:lifetime, $kind:path, [$(#[$attr:meta])*], $var:ident, $ty:ty, $inner:path) => { + $(#[$attr])* impl<$($p)*> $crate::templater::WrapTemplateProperty<$a, $ty> for $kind { fn wrap_property(property: $crate::templater::BoxedTemplateProperty<$a, $ty>) -> Self { Self::$var($inner(property)) diff --git a/cli/tests/cli-reference@.md.snap b/cli/tests/cli-reference@.md.snap index 38a5d3d03a1..aa815ae40f7 100644 --- a/cli/tests/cli-reference@.md.snap +++ b/cli/tests/cli-reference@.md.snap @@ -1756,6 +1756,17 @@ If a working-copy commit gets abandoned, it will be given a new, empty commit. T [string pattern syntax]: https://docs.jj-vcs.dev/latest/revsets/#string-patterns * `--all-remotes` — Fetch from all remotes +* `-T`, `--template