867 lines · 31.6 KB · Rust
RawDownload
// 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::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>,     /// Don't show the graph, show a flat list of modified changes    #[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 merged_from_op = repo_loader.merge_operations(from_ops.clone(), None).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 =        op_diff_changes_expr.resolve_user_expression(from_repo, &from_repo_symbol_resolver)?;    let to_op_diff_changes_expr =        op_diff_changes_expr.resolve_user_expression(to_repo, &to_repo_symbol_resolver)?;    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                })                .await?;        }    }     let ignored_remote = default_ignored_remote_name(current_repo.store());    let get_remote_ref_prefix = |remote_ref: &RemoteRef| match remote_ref.state {        RemoteRefState::New => "untracked",        RemoteRefState::Tracked => "tracked",    };     let changed_remote_bookmarks = diff_named_remote_refs(        from_repo.view().all_remote_bookmarks(),        to_repo.view().all_remote_bookmarks(),    )

Lines 1–500 of 867.