297 lines · 10.8 KB · Rust
RawDownload
// Copyright 2020-2023 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::slice; use clap_complete::ArgValueCandidates;use futures::StreamExt as _;use futures::TryStreamExt as _;use futures::stream;use futures::stream::LocalBoxStream;use jj_lib::graph::GraphEdge;use jj_lib::graph::reverse_graph;use jj_lib::op_walk;use jj_lib::operation::Operation;use jj_lib::repo::RepoLoader; use super::diff::parse_op_diff_changes_in;use super::diff::show_op_diff;use crate::cli_util::CommandHelper;use crate::cli_util::LogContentFormat;use crate::cli_util::WorkspaceCommandEnvironment;use crate::cli_util::format_template;use crate::cli_util::merge_operations;use crate::command_error::CommandError;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::graphlog::GraphStyle;use crate::graphlog::get_graphlog;use crate::operation_templater::OperationTemplateLanguage;use crate::templater::TemplateRenderer;use crate::ui::Ui; /// Show the operation log////// Like other commands, `jj op log` snapshots the current working-copy changes/// and reconciles divergent operations. Use `--at-op=@ --ignore-working-copy`/// to inspect the current state without mutation.#[derive(clap::Args, Clone, Debug)]pub struct OperationLogArgs {    /// Limit number of operations to show    ///    /// Applied after operations are reordered topologically, but before being    /// reversed.    #[arg(long, short = 'n')]    limit: Option<usize>,     /// Show operations in the opposite order (older operations first)    #[arg(long)]    reversed: bool,     /// Don't show the graph, show a flat list of operations    #[arg(long, short = 'G')]    no_graph: bool,     /// Render each operation using the given template    ///    /// You can specify arbitrary template expressions using the    /// [built-in keywords]. See [`jj help -k templates`] for more    /// information.    ///    /// [built-in keywords]:    ///     https://docs.jj-vcs.dev/latest/templates/#operation-keywords    ///    /// [`jj help -k templates`]:    ///     https://docs.jj-vcs.dev/latest/templates/    #[arg(long, short = 'T')]    #[arg(add = ArgValueCandidates::new(complete::template_aliases))]    template: Option<String>,     /// Show changes to the repository at each operation    #[arg(long, short = 'd')]    op_diff: bool,     /// Show patch of modifications to changes (implies --op-diff)    ///    /// 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_log(    ui: &mut Ui,    command: &CommandHelper,    args: &OperationLogArgs,) -> Result<(), CommandError> {    if command.is_working_copy_writable() {        let workspace_command = command.workspace_helper(ui).await?;        let current_op = workspace_command.repo().operation();        let repo_loader = workspace_command.workspace().repo_loader();        do_op_log(ui, workspace_command.env(), repo_loader, current_op, args).await    } else {        // Don't load the repo so that the operation history can be inspected        // even with a corrupted repo state. For example, you can find the first        // bad operation id to be abandoned.        let workspace = command.load_workspace()?;        let workspace_env = command.workspace_environment(ui, &workspace)?;        let repo_loader = workspace.repo_loader();        let current_op =            command.resolve_operation(ui, workspace.repo_loader(), workspace.workspace_name())?;        do_op_log(ui, &workspace_env, repo_loader, &current_op, args).await    }} async fn do_op_log(    ui: &mut Ui,    workspace_env: &WorkspaceCommandEnvironment,    repo_loader: &RepoLoader,    current_op: &Operation,    args: &OperationLogArgs,) -> Result<(), CommandError> {    let settings = repo_loader.settings();    let graph_style = GraphStyle::from_settings(settings)?;    let with_content_format = LogContentFormat::new(ui, settings)?;     let template: TemplateRenderer<Operation>;    let op_node_template: TemplateRenderer<Operation>;    {        let language = OperationTemplateLanguage::new(            repo_loader,            Some(current_op.id()),            workspace_env.cwd(),            workspace_env.operation_template_extensions(),        );        let text = match &args.template {            Some(value) => value.to_owned(),            None => settings.get_string("templates.op_log")?,        };        template = workspace_env            .parse_template(ui, &language, &text)?            .labeled(["op_log", "operation"]);        op_node_template = workspace_env            .parse_template(                ui,                &language,                &settings.get_string("templates.op_log_node")?,            )?            .labeled(["op_log", "operation", "node"]);    }     let diff_formats = diff_formats_for_log(settings, &args.diff_format, args.patch)?;    let maybe_show_op_diff = if args.op_diff || !diff_formats.is_empty() {        let template_text = settings.get_string("templates.commit_summary")?;        let op_diff_changes_expr =            parse_op_diff_changes_in(ui, settings, workspace_env, args.show_changes_in.as_deref())?;        let show = async move |ui: &Ui,                               formatter: &mut dyn Formatter,                               op: &Operation,                               with_content_format: &LogContentFormat| {            let parent_ops = op.parents().await?;            let workspace_name = None;            let transaction_description = None;            let command_args = [];            let merged_parent_op = merge_operations(                None,                repo_loader,                parent_ops.clone(),                workspace_name,                transaction_description,                &command_args,            )            .await?;            let parent_repo = repo_loader.load_at(&merged_parent_op).await?;            let repo = repo_loader.load_at(op).await?;             let id_prefix_context = workspace_env.new_id_prefix_context();            let commit_summary_template = {                let language =                    workspace_env.commit_template_language(repo.as_ref(), &id_prefix_context);                workspace_env                    .parse_template(ui, &language, &template_text)?                    .labeled(["op_log", "commit"])            };            let path_converter = workspace_env.path_converter();            let conflict_marker_style = workspace_env.conflict_marker_style();            let diff_renderer = (!diff_formats.is_empty()).then(|| {                DiffRenderer::new(                    repo.as_ref(),                    path_converter,                    conflict_marker_style,                    diff_formats.clone(),                )            });             // TODO: Merged repo may have newly rebased commits, which wouldn't            // exist in the index. (#4465)            if parent_ops.len() > 1 {                return Ok(());            }            show_op_diff(                ui,                workspace_env,                formatter,                repo.as_ref(),                &parent_repo,                &repo,                &commit_summary_template,                (!args.no_graph).then_some(graph_style),                with_content_format,                diff_renderer.as_ref(),                op_diff_changes_expr.clone(),            )            .await        };        Some(show)    } else {        None    };     ui.request_pager();    let mut formatter = ui.stdout_formatter();    let formatter = formatter.as_mut();    let stream =        op_walk::walk_ancestors(slice::from_ref(current_op)).take(args.limit.unwrap_or(usize::MAX));     if !args.no_graph {        let mut raw_output = formatter.raw()?;        let mut graph = get_graphlog(graph_style, raw_output.as_mut());        let stream = stream.map_ok(|op| {            let ids = op.parent_ids();            let edges = ids.iter().cloned().map(GraphEdge::direct).collect();            (op, edges)        });        let mut stream_nodes: LocalBoxStream<'_, _> = if args.reversed {            stream::iter(                reverse_graph(stream.collect::<Vec<_>>().await.into_iter(), Operation::id)?                    .into_iter()                    .map(Ok),            )            .boxed()        } else {            stream.boxed_local()        };        while let Some(node) = stream_nodes.next().await {            let (op, edges) = node?;            let mut buffer = vec![];            let within_graph = with_content_format.sub_width(graph.width(op.id(), &edges));            within_graph                .write(ui.new_formatter(&mut buffer).as_mut(), async |formatter| {                    template.format(&op, formatter)                })                .await?;            if let Some(show) = &maybe_show_op_diff {                let mut formatter = ui.new_formatter(&mut buffer);                show(ui, formatter.as_mut(), &op, &within_graph).await?;            }            let node_symbol = format_template(ui, &op, &op_node_template);            graph.add_node(                op.id(),                &edges,                &node_symbol,                &String::from_utf8_lossy(&buffer),            )?;        }    } else {        let mut stream: LocalBoxStream<'_, _> = if args.reversed {            stream::iter(stream.collect::<Vec<_>>().await.into_iter().rev()).boxed()        } else {            stream.boxed_local()        };        while let Some(op) = stream.try_next().await? {            with_content_format                .write(formatter, async |formatter| template.format(&op, formatter))                .await?;            if let Some(show) = &maybe_show_op_diff {                show(ui, formatter, &op, &with_content_format).await?;            }        }    }     Ok(())}