// Copyright 2024 The Jujutsu Authors//// Licensed under the Apache License, Version 2.0 (the "License");// you may not use this file except in compliance with the License.// You may obtain a copy of the License at//// https://www.apache.org/licenses/LICENSE-2.0//// Unless required by applicable law or agreed to in writing, software// distributed under the License is distributed on an "AS IS" BASIS,// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.// See the License for the specific language governing permissions and// limitations under the License. use std::collections::HashMap;use std::collections::HashSet;use std::slice;use std::sync::Arc; use clap_complete::ArgValueCandidates;use futures::StreamExt as _;use futures::TryStreamExt as _;use futures::future::try_join_all;use itertools::Itertools as _;use jj_lib::backend::ChangeId;use jj_lib::backend::CommitId;use jj_lib::commit::Commit;use jj_lib::evolution::accumulate_predecessors;use jj_lib::graph::TopoGroupedGraph;use jj_lib::matchers::EverythingMatcher;use jj_lib::op_store::RefTarget;use jj_lib::op_store::RemoteRef;use jj_lib::op_store::RemoteRefState;use jj_lib::refs::diff_named_commit_ids;use jj_lib::refs::diff_named_ref_targets;use jj_lib::refs::diff_named_remote_refs;use jj_lib::repo::ReadonlyRepo;use jj_lib::repo::Repo;use jj_lib::revset;use jj_lib::revset::ResolvedRevsetExpression;use jj_lib::revset::RevsetDiagnostics;use jj_lib::revset::RevsetExpression;use jj_lib::revset::RevsetResolutionError;use jj_lib::revset::SymbolResolver;use jj_lib::revset::UserRevsetExpression;use jj_lib::settings::UserSettings; use crate::cli_util::CommandHelper;use crate::cli_util::LogContentFormat;use crate::cli_util::WorkspaceCommandEnvironment;use crate::cli_util::default_ignored_remote_name;use crate::cli_util::merge_operations;use crate::command_error::CommandError;use crate::command_error::config_error_with_message;use crate::command_error::print_parse_diagnostics;use crate::command_error::user_error_with_message;use crate::complete;use crate::diff_util::DiffFormatArgs;use crate::diff_util::DiffRenderer;use crate::diff_util::diff_formats_for_log;use crate::formatter::Formatter;use crate::formatter::FormatterExt as _;use crate::graphlog::GraphStyle;use crate::graphlog::get_graphlog;use crate::templater::TemplateRenderer;use crate::ui::Ui; /// Compare changes to the repository between two operations#[derive(clap::Args, Clone, Debug)]pub struct OperationDiffArgs { /// Show repository changes in this operation, compared to its parent #[arg(long, visible_alias = "op")] #[arg(add = ArgValueCandidates::new(complete::operations))] operation: Option<String>, /// Show repository changes from this operation #[arg(long, short, conflicts_with = "operation")] #[arg(add = ArgValueCandidates::new(complete::operations))] from: Option<String>, /// Show repository changes to this operation #[arg(long, short, conflicts_with = "operation")] #[arg(add = ArgValueCandidates::new(complete::operations))] to: Option<String>, /// Show a flat list of modified changes instead of a graph #[arg(long, short = 'G')] no_graph: bool, /// Show patch of modifications to changes /// /// If the previous version has different parents, it will be temporarily /// rebased to the parents of the new version, so the diff is not /// contaminated by unrelated changes. #[arg(long, short = 'p')] patch: bool, #[command(flatten)] diff_format: DiffFormatArgs, /// Show only changed revisions matching the given revset expression /// /// If no revisions are specified, this defaults to the /// `revsets.op-diff-changes-in` setting. #[arg(long, value_name = "REVSETS")] show_changes_in: Option<String>,} pub async fn cmd_op_diff( ui: &mut Ui, command: &CommandHelper, args: &OperationDiffArgs,) -> Result<(), CommandError> { let workspace_command = command.workspace_helper(ui).await?; let workspace_env = workspace_command.env(); let repo_loader = workspace_command.workspace().repo_loader(); let settings = workspace_command.settings(); let from_ops; let to_op; if args.from.is_some() || args.to.is_some() { from_ops = vec![workspace_command.resolve_single_op(args.from.as_deref().unwrap_or("@"))?]; to_op = workspace_command.resolve_single_op(args.to.as_deref().unwrap_or("@"))?; } else { to_op = workspace_command.resolve_single_op(args.operation.as_deref().unwrap_or("@"))?; from_ops = to_op.parents().await?; } let graph_style = GraphStyle::from_settings(settings)?; let with_content_format = LogContentFormat::new(ui, settings)?; let workspace_name = None; let transaction_description = None; let command_args = []; let merged_from_op = merge_operations( None, repo_loader, from_ops.clone(), workspace_name, transaction_description, &command_args, ) .await?; let from_repo = repo_loader.load_at(&merged_from_op).await?; let to_repo = repo_loader.load_at(&to_op).await?; // Create a new transaction starting from `to_repo`. let mut tx = to_repo.start_transaction(); // Merge index from `from_repo` to `to_repo`, so commits in `from_repo` are // accessible. tx.repo_mut().merge_index(&from_repo)?; let merged_repo = tx.repo(); let diff_renderer = { let formats = diff_formats_for_log(settings, &args.diff_format, args.patch)?; let path_converter = workspace_env.path_converter(); let conflict_marker_style = workspace_env.conflict_marker_style(); (!formats.is_empty()) .then(|| DiffRenderer::new(merged_repo, path_converter, conflict_marker_style, formats)) }; let id_prefix_context = workspace_env.new_id_prefix_context(); let commit_summary_template = { let language = workspace_env.commit_template_language(merged_repo, &id_prefix_context); let text = settings.get_string("templates.commit_summary")?; workspace_env .parse_template(ui, &language, &text)? .labeled(["op_diff", "commit"]) }; let op_diff_changes_expr = parse_op_diff_changes_in(ui, settings, workspace_env, args.show_changes_in.as_deref())?; let op_summary_template = workspace_command .operation_summary_template() .labeled(["op_diff"]); ui.request_pager(); let mut formatter = ui.stdout_formatter(); for op in &from_ops { write!(formatter, "From operation: ")?; op_summary_template.format(op, &mut *formatter)?; writeln!(formatter)?; } // "From operation: " write!(formatter, " To operation: ")?; op_summary_template.format(&to_op, &mut *formatter)?; writeln!(formatter)?; show_op_diff( ui, workspace_env, formatter.as_mut(), merged_repo, &from_repo, &to_repo, &commit_summary_template, (!args.no_graph).then_some(graph_style), &with_content_format, diff_renderer.as_ref(), op_diff_changes_expr, ) .await} /// Parses the revset expression used to filter revisions in operation diffs.pub fn parse_op_diff_changes_in( ui: &Ui, settings: &UserSettings, workspace_env: &WorkspaceCommandEnvironment, show_changes_in: Option<&str>,) -> Result<Arc<UserRevsetExpression>, CommandError> { let (expression_str, is_config) = if let Some(show_changes_in_expr) = show_changes_in { (show_changes_in_expr.to_string(), false) } else { (settings.get("revsets.op-diff-changes-in")?, true) }; let mut diagnostics = RevsetDiagnostics::new(); let op_diff_changes_expr = revset::parse( &mut diagnostics, &expression_str, &workspace_env.revset_parse_context(), ) .map_err(|err| { if is_config { config_error_with_message("Invalid `revsets.op-diff-changes-in`", err) } else { user_error_with_message( format!("Invalid `--show-changes-in` expression: {expression_str}"), err, ) } })?; let context_message = if is_config { "In `revsets.op-diff-changes-in`" } else { "In `--show-changes-in`" }; print_parse_diagnostics(ui, context_message, &diagnostics)?; Ok(op_diff_changes_expr)} /// Resolves the `op-diff-changes-in` expression for both the "from" and "to"/// repositories.fn resolve_op_diff_changes_exprs( workspace_env: &WorkspaceCommandEnvironment, op_diff_changes_expr: &UserRevsetExpression, from_repo: &ReadonlyRepo, to_repo: &ReadonlyRepo,) -> Result<(Arc<ResolvedRevsetExpression>, Arc<ResolvedRevsetExpression>), RevsetResolutionError> { let extensions = workspace_env .revset_parse_context() .extensions .symbol_resolvers(); let from_repo_symbol_resolver = SymbolResolver::new(from_repo, extensions); let to_repo_symbol_resolver = SymbolResolver::new(to_repo, extensions); let (from_op_diff_changes_expr, _other_repos) = op_diff_changes_expr .resolve_user_expression(from_repo, &from_repo_symbol_resolver)? .into_inner(); let (to_op_diff_changes_expr, _other_repos) = op_diff_changes_expr .resolve_user_expression(to_repo, &to_repo_symbol_resolver)? .into_inner(); Ok((from_op_diff_changes_expr, to_op_diff_changes_expr))} /// Computes and shows the differences between two operations, using the given/// `ReadonlyRepo`s for the operations./// `current_repo` should contain a `Repo` with the indices of both repos merged/// into it.#[expect(clippy::too_many_arguments)]pub async fn show_op_diff( ui: &Ui, workspace_env: &WorkspaceCommandEnvironment, formatter: &mut dyn Formatter, current_repo: &dyn Repo, from_repo: &Arc<ReadonlyRepo>, to_repo: &Arc<ReadonlyRepo>, commit_summary_template: &TemplateRenderer<'_, Commit>, graph_style: Option<GraphStyle>, with_content_format: &LogContentFormat, diff_renderer: Option<&DiffRenderer<'_>>, op_diff_changes_expr: Arc<UserRevsetExpression>,) -> Result<(), CommandError> { let op_commits_diff_result = match resolve_op_diff_changes_exprs( workspace_env, &op_diff_changes_expr, from_repo.as_ref(), to_repo.as_ref(), ) { Ok((from_op_diff_changes_expr, to_op_diff_changes_expr)) => { let op_commits_diff = compute_operation_commits_diff( current_repo, from_repo, to_repo, from_op_diff_changes_expr, to_op_diff_changes_expr, ) .await?; Some(op_commits_diff) } Err(err) => { writeln!(formatter)?; with_content_format .write(formatter, async |formatter| { writeln!( formatter.labeled("warning"), "Warning: Could not resolve revset expression for elision: {err}" )?; writeln!( formatter, " (Use --show-changes-in=all() to see all changes)" ) }) .await?; None } }; if let Some(op_commits_diff) = op_commits_diff_result && op_commits_diff.has_changes() { let revset = RevsetExpression::commits(op_commits_diff.changes.keys().cloned().collect()) .evaluate(current_repo)?; writeln!(formatter)?; with_content_format .write(formatter, async |formatter| { writeln!(formatter, "Changed commits:") }) .await?; if let Some(graph_style) = graph_style { let mut raw_output = formatter.raw()?; let mut graph = get_graphlog(graph_style, raw_output.as_mut()); let mut graph_stream = TopoGroupedGraph::new(revset.stream_graph(), |id| id) .stream() .boxed_local(); while let Some((commit_id, mut edges)) = graph_stream.try_next().await? { let modified_change = op_commits_diff.changes.get(&commit_id).unwrap(); // Omit "missing" edge to keep the graph concise. edges.retain(|edge| !edge.is_missing()); let mut buffer = vec![]; let within_graph = with_content_format.sub_width(graph.width(&commit_id, &edges)); within_graph .write(ui.new_formatter(&mut buffer).as_mut(), async |formatter| { write_modified_change_summary( formatter, commit_summary_template, modified_change, ) }) .await?; if let Some(diff_renderer) = diff_renderer { let mut formatter = ui.new_formatter(&mut buffer); show_change_diff( ui, formatter.as_mut(), diff_renderer, modified_change, within_graph.width(), ) .await?; } // TODO: customize node symbol? let node_symbol = "○"; graph.add_node( &commit_id, &edges, node_symbol, &String::from_utf8_lossy(&buffer), )?; } } else { let mut commit_ids = revset.stream(); while let Some(commit_id) = commit_ids.try_next().await? { let modified_change = op_commits_diff.changes.get(&commit_id).unwrap(); with_content_format .write(formatter, async |formatter| { write_modified_change_summary( formatter, commit_summary_template, modified_change, ) }) .await?; if let Some(diff_renderer) = diff_renderer { let width = with_content_format.width(); show_change_diff(ui, formatter, diff_renderer, modified_change, width).await?; } } } write_elided_commit_counts(formatter, with_content_format, &op_commits_diff).await?; } let changed_working_copies = diff_named_commit_ids( from_repo.view().wc_commit_ids(), to_repo.view().wc_commit_ids(), ) .collect_vec(); if !changed_working_copies.is_empty() { writeln!(formatter)?; for (name, (from_commit, to_commit)) in changed_working_copies { with_content_format .write(formatter, async |formatter| { // Usually, there is at most one working copy changed per operation, so we put // the working copy name in the heading. write!(formatter, "Changed working copy ")?; write!(formatter.labeled("working_copies"), "{}@", name.as_symbol())?; writeln!(formatter, ":")?; write_ref_target_summary( formatter, current_repo, commit_summary_template, &RefTarget::resolved(to_commit.cloned()), true, None, ) .await?; write_ref_target_summary( formatter, current_repo, commit_summary_template, &RefTarget::resolved(from_commit.cloned()), false, None, ) .await }) .await?; } } let changed_local_bookmarks = diff_named_ref_targets( from_repo.view().local_bookmarks(), to_repo.view().local_bookmarks(), ) .collect_vec(); if !changed_local_bookmarks.is_empty() { writeln!(formatter)?; with_content_format .write(formatter, async |formatter| { writeln!(formatter, "Changed local bookmarks:") }) .await?; for (name, (from_target, to_target)) in changed_local_bookmarks { with_content_format .write(formatter, async |formatter| { writeln!(formatter, "{name}:", name = name.as_symbol())?; write_ref_target_summary( formatter, current_repo, commit_summary_template, to_target, true, None, ) .await?; write_ref_target_summary( formatter, current_repo, commit_summary_template, from_target, false, None, ) .await }) .await?; } } let changed_local_tags = diff_named_ref_targets(from_repo.view().local_tags(), to_repo.view().local_tags()) .collect_vec(); if !changed_local_tags.is_empty() { writeln!(formatter)?; with_content_format .write(formatter, async |formatter| { writeln!(formatter, "Changed local tags:") }) .await?; for (name, (from_target, to_target)) in changed_local_tags { with_content_format .write(formatter, async |formatter| { writeln!(formatter, "{name}:", name = name.as_symbol())?; write_ref_target_summary( formatter, current_repo, commit_summary_template, to_target, true, None, ) .await?; write_ref_target_summary( formatter, current_repo, commit_summary_template, from_target, false, None, ) .await })Lines 1–500 of 881. Load more
// Copyright 2024 The Jujutsu Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // https://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. use std::collections::HashMap; use std::collections::HashSet; use std::slice; use std::sync::Arc; use clap_complete::ArgValueCandidates; use futures::StreamExt as _; use futures::TryStreamExt as _; use futures::future::try_join_all; use itertools::Itertools as _; use jj_lib::backend::ChangeId; use jj_lib::backend::CommitId; use jj_lib::commit::Commit; use jj_lib::evolution::accumulate_predecessors; use jj_lib::graph::TopoGroupedGraph; use jj_lib::matchers::EverythingMatcher; use jj_lib::op_store::RefTarget; use jj_lib::op_store::RemoteRef; use jj_lib::op_store::RemoteRefState; use jj_lib::refs::diff_named_commit_ids; use jj_lib::refs::diff_named_ref_targets; use jj_lib::refs::diff_named_remote_refs; use jj_lib::repo::ReadonlyRepo; use jj_lib::repo::Repo; use jj_lib::revset; use jj_lib::revset::ResolvedRevsetExpression; use jj_lib::revset::RevsetDiagnostics; use jj_lib::revset::RevsetExpression; use jj_lib::revset::RevsetResolutionError; use jj_lib::revset::SymbolResolver; use jj_lib::revset::UserRevsetExpression; use jj_lib::settings::UserSettings; use crate::cli_util::CommandHelper; use crate::cli_util::LogContentFormat; use crate::cli_util::WorkspaceCommandEnvironment; use crate::cli_util::default_ignored_remote_name; use crate::cli_util::merge_operations; use crate::command_error::CommandError; use crate::command_error::config_error_with_message; use crate::command_error::print_parse_diagnostics; use crate::command_error::user_error_with_message; use crate::complete; use crate::diff_util::DiffFormatArgs; use crate::diff_util::DiffRenderer; use crate::diff_util::diff_formats_for_log; use crate::formatter::Formatter; use crate::formatter::FormatterExt as _; use crate::graphlog::GraphStyle; use crate::graphlog::get_graphlog; use crate::templater::TemplateRenderer; use crate::ui::Ui; /// Compare changes to the repository between two operations #[derive(clap::Args, Clone, Debug)] pub struct OperationDiffArgs { /// Show repository changes in this operation, compared to its parent #[arg(long, visible_alias = "op")] #[arg(add = ArgValueCandidates::new(complete::operations))] operation: Option<String>, /// Show repository changes from this operation #[arg(long, short, conflicts_with = "operation")] #[arg(add = ArgValueCandidates::new(complete::operations))] from: Option<String>, /// Show repository changes to this operation #[arg(long, short, conflicts_with = "operation")] #[arg(add = ArgValueCandidates::new(complete::operations))] to: Option<String>, /// Show a flat list of modified changes instead of a graph #[arg(long, short = 'G')] no_graph: bool, /// Show patch of modifications to changes /// /// If the previous version has different parents, it will be temporarily /// rebased to the parents of the new version, so the diff is not /// contaminated by unrelated changes. #[arg(long, short = 'p')] patch: bool, #[command(flatten)] diff_format: DiffFormatArgs, /// Show only changed revisions matching the given revset expression /// /// If no revisions are specified, this defaults to the /// `revsets.op-diff-changes-in` setting. #[arg(long, value_name = "REVSETS")] show_changes_in: Option<String>, } pub async fn cmd_op_diff( ui: &mut Ui, command: &CommandHelper, args: &OperationDiffArgs, ) -> Result<(), CommandError> { let workspace_command = command.workspace_helper(ui).await?; let workspace_env = workspace_command.env(); let repo_loader = workspace_command.workspace().repo_loader(); let settings = workspace_command.settings(); let from_ops; let to_op; if args.from.is_some() || args.to.is_some() { from_ops = vec![workspace_command.resolve_single_op(args.from.as_deref().unwrap_or("@"))?]; to_op = workspace_command.resolve_single_op(args.to.as_deref().unwrap_or("@"))?; } else { to_op = workspace_command.resolve_single_op(args.operation.as_deref().unwrap_or("@"))?; from_ops = to_op.parents().await?; } let graph_style = GraphStyle::from_settings(settings)?; let with_content_format = LogContentFormat::new(ui, settings)?; let workspace_name = None; let transaction_description = None; let command_args = []; let merged_from_op = merge_operations( None, repo_loader, from_ops.clone(), workspace_name, transaction_description, &command_args, ) .await?; let from_repo = repo_loader.load_at(&merged_from_op).await?; let to_repo = repo_loader.load_at(&to_op).await?; // Create a new transaction starting from `to_repo`. let mut tx = to_repo.start_transaction(); // Merge index from `from_repo` to `to_repo`, so commits in `from_repo` are // accessible. tx.repo_mut().merge_index(&from_repo)?; let merged_repo = tx.repo(); let diff_renderer = { let formats = diff_formats_for_log(settings, &args.diff_format, args.patch)?; let path_converter = workspace_env.path_converter(); let conflict_marker_style = workspace_env.conflict_marker_style(); (!formats.is_empty()) .then(|| DiffRenderer::new(merged_repo, path_converter, conflict_marker_style, formats)) }; let id_prefix_context = workspace_env.new_id_prefix_context(); let commit_summary_template = { let language = workspace_env.commit_template_language(merged_repo, &id_prefix_context); let text = settings.get_string("templates.commit_summary")?; workspace_env .parse_template(ui, &language, &text)? .labeled(["op_diff", "commit"]) }; let op_diff_changes_expr = parse_op_diff_changes_in(ui, settings, workspace_env, args.show_changes_in.as_deref())?; let op_summary_template = workspace_command .operation_summary_template() .labeled(["op_diff"]); ui.request_pager(); let mut formatter = ui.stdout_formatter(); for op in &from_ops { write!(formatter, "From operation: ")?; op_summary_template.format(op, &mut *formatter)?; writeln!(formatter)?; } // "From operation: " write!(formatter, " To operation: ")?; op_summary_template.format(&to_op, &mut *formatter)?; writeln!(formatter)?; show_op_diff( ui, workspace_env, formatter.as_mut(), merged_repo, &from_repo, &to_repo, &commit_summary_template, (!args.no_graph).then_some(graph_style), &with_content_format, diff_renderer.as_ref(), op_diff_changes_expr, ) .await } /// Parses the revset expression used to filter revisions in operation diffs. pub fn parse_op_diff_changes_in( ui: &Ui, settings: &UserSettings, workspace_env: &WorkspaceCommandEnvironment, show_changes_in: Option<&str>, ) -> Result<Arc<UserRevsetExpression>, CommandError> { let (expression_str, is_config) = if let Some(show_changes_in_expr) = show_changes_in { (show_changes_in_expr.to_string(), false) } else { (settings.get("revsets.op-diff-changes-in")?, true) }; let mut diagnostics = RevsetDiagnostics::new(); let op_diff_changes_expr = revset::parse( &mut diagnostics, &expression_str, &workspace_env.revset_parse_context(), ) .map_err(|err| { if is_config { config_error_with_message("Invalid `revsets.op-diff-changes-in`", err) } else { user_error_with_message( format!("Invalid `--show-changes-in` expression: {expression_str}"), err, ) } })?; let context_message = if is_config { "In `revsets.op-diff-changes-in`" } else { "In `--show-changes-in`" }; print_parse_diagnostics(ui, context_message, &diagnostics)?; Ok(op_diff_changes_expr) } /// Resolves the `op-diff-changes-in` expression for both the "from" and "to" /// repositories. fn resolve_op_diff_changes_exprs( workspace_env: &WorkspaceCommandEnvironment, op_diff_changes_expr: &UserRevsetExpression, from_repo: &ReadonlyRepo, to_repo: &ReadonlyRepo, ) -> Result<(Arc<ResolvedRevsetExpression>, Arc<ResolvedRevsetExpression>), RevsetResolutionError> { let extensions = workspace_env .revset_parse_context() .extensions .symbol_resolvers(); let from_repo_symbol_resolver = SymbolResolver::new(from_repo, extensions); let to_repo_symbol_resolver = SymbolResolver::new(to_repo, extensions); let (from_op_diff_changes_expr, _other_repos) = op_diff_changes_expr .resolve_user_expression(from_repo, &from_repo_symbol_resolver)? .into_inner(); let (to_op_diff_changes_expr, _other_repos) = op_diff_changes_expr .resolve_user_expression(to_repo, &to_repo_symbol_resolver)? .into_inner(); Ok((from_op_diff_changes_expr, to_op_diff_changes_expr)) } /// Computes and shows the differences between two operations, using the given /// `ReadonlyRepo`s for the operations. /// `current_repo` should contain a `Repo` with the indices of both repos merged /// into it. #[expect(clippy::too_many_arguments)] pub async fn show_op_diff( ui: &Ui, workspace_env: &WorkspaceCommandEnvironment, formatter: &mut dyn Formatter, current_repo: &dyn Repo, from_repo: &Arc<ReadonlyRepo>, to_repo: &Arc<ReadonlyRepo>, commit_summary_template: &TemplateRenderer<'_, Commit>, graph_style: Option<GraphStyle>, with_content_format: &LogContentFormat, diff_renderer: Option<&DiffRenderer<'_>>, op_diff_changes_expr: Arc<UserRevsetExpression>, ) -> Result<(), CommandError> { let op_commits_diff_result = match resolve_op_diff_changes_exprs( workspace_env, &op_diff_changes_expr, from_repo.as_ref(), to_repo.as_ref(), ) { Ok((from_op_diff_changes_expr, to_op_diff_changes_expr)) => { let op_commits_diff = compute_operation_commits_diff( current_repo, from_repo, to_repo, from_op_diff_changes_expr, to_op_diff_changes_expr, ) .await?; Some(op_commits_diff) } Err(err) => { writeln!(formatter)?; with_content_format .write(formatter, async |formatter| { writeln!( formatter.labeled("warning"), "Warning: Could not resolve revset expression for elision: {err}" )?; writeln!( formatter, " (Use --show-changes-in=all() to see all changes)" ) }) .await?; None } }; if let Some(op_commits_diff) = op_commits_diff_result && op_commits_diff.has_changes() { let revset = RevsetExpression::commits(op_commits_diff.changes.keys().cloned().collect()) .evaluate(current_repo)?; writeln!(formatter)?; with_content_format .write(formatter, async |formatter| { writeln!(formatter, "Changed commits:") }) .await?; if let Some(graph_style) = graph_style { let mut raw_output = formatter.raw()?; let mut graph = get_graphlog(graph_style, raw_output.as_mut()); let mut graph_stream = TopoGroupedGraph::new(revset.stream_graph(), |id| id) .stream() .boxed_local(); while let Some((commit_id, mut edges)) = graph_stream.try_next().await? { let modified_change = op_commits_diff.changes.get(&commit_id).unwrap(); // Omit "missing" edge to keep the graph concise. edges.retain(|edge| !edge.is_missing()); let mut buffer = vec![]; let within_graph = with_content_format.sub_width(graph.width(&commit_id, &edges)); within_graph .write(ui.new_formatter(&mut buffer).as_mut(), async |formatter| { write_modified_change_summary( formatter, commit_summary_template, modified_change, ) }) .await?; if let Some(diff_renderer) = diff_renderer { let mut formatter = ui.new_formatter(&mut buffer); show_change_diff( ui, formatter.as_mut(), diff_renderer, modified_change, within_graph.width(), ) .await?; } // TODO: customize node symbol? let node_symbol = "○"; graph.add_node( &commit_id, &edges, node_symbol, &String::from_utf8_lossy(&buffer), )?; } } else { let mut commit_ids = revset.stream(); while let Some(commit_id) = commit_ids.try_next().await? { let modified_change = op_commits_diff.changes.get(&commit_id).unwrap(); with_content_format .write(formatter, async |formatter| { write_modified_change_summary( formatter, commit_summary_template, modified_change, ) }) .await?; if let Some(diff_renderer) = diff_renderer { let width = with_content_format.width(); show_change_diff(ui, formatter, diff_renderer, modified_change, width).await?; } } } write_elided_commit_counts(formatter, with_content_format, &op_commits_diff).await?; } let changed_working_copies = diff_named_commit_ids( from_repo.view().wc_commit_ids(), to_repo.view().wc_commit_ids(), ) .collect_vec(); if !changed_working_copies.is_empty() { writeln!(formatter)?; for (name, (from_commit, to_commit)) in changed_working_copies { with_content_format .write(formatter, async |formatter| { // Usually, there is at most one working copy changed per operation, so we put // the working copy name in the heading. write!(formatter, "Changed working copy ")?; write!(formatter.labeled("working_copies"), "{}@", name.as_symbol())?; writeln!(formatter, ":")?; write_ref_target_summary( formatter, current_repo, commit_summary_template, &RefTarget::resolved(to_commit.cloned()), true, None, ) .await?; write_ref_target_summary( formatter, current_repo, commit_summary_template, &RefTarget::resolved(from_commit.cloned()), false, None, ) .await }) .await?; } } let changed_local_bookmarks = diff_named_ref_targets( from_repo.view().local_bookmarks(), to_repo.view().local_bookmarks(), ) .collect_vec(); if !changed_local_bookmarks.is_empty() { writeln!(formatter)?; with_content_format .write(formatter, async |formatter| { writeln!(formatter, "Changed local bookmarks:") }) .await?; for (name, (from_target, to_target)) in changed_local_bookmarks { with_content_format .write(formatter, async |formatter| { writeln!(formatter, "{name}:", name = name.as_symbol())?; write_ref_target_summary( formatter, current_repo, commit_summary_template, to_target, true, None, ) .await?; write_ref_target_summary( formatter, current_repo, commit_summary_template, from_target, false, None, ) .await }) .await?; } } let changed_local_tags = diff_named_ref_targets(from_repo.view().local_tags(), to_repo.view().local_tags()) .collect_vec(); if !changed_local_tags.is_empty() { writeln!(formatter)?; with_content_format .write(formatter, async |formatter| { writeln!(formatter, "Changed local tags:") }) .await?; for (name, (from_target, to_target)) in changed_local_tags { with_content_format .write(formatter, async |formatter| { writeln!(formatter, "{name}:", name = name.as_symbol())?; write_ref_target_summary( formatter, current_repo, commit_summary_template, to_target, true, None, ) .await?; write_ref_target_summary( formatter, current_repo, commit_summary_template, from_target, false, None, ) .await })