// Copyright 2020 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::io; use clap_complete::ArgValueCandidates; use clap_complete::ArgValueCompleter; use itertools::Itertools as _; use jj_lib::commit::Commit; use jj_lib::evolution::walk_predecessors; use jj_lib::graph::reverse_graph; use jj_lib::graph::GraphEdge; use jj_lib::graph::TopoGroupedGraphIterator; use jj_lib::matchers::EverythingMatcher; use tracing::instrument; use crate::cli_util::format_template; use crate::cli_util::CommandHelper; use crate::cli_util::LogContentFormat; use crate::cli_util::RevisionArg; use crate::command_error::CommandError; use crate::complete; use crate::diff_util::DiffFormatArgs; use crate::graphlog::get_graphlog; use crate::graphlog::GraphStyle; use crate::templater::TemplateRenderer; use crate::ui::Ui; /// Show how a change has evolved over time /// /// Lists the previous commits which a change has pointed to. The current commit /// of a change evolves when the change is updated, rebased, etc. #[derive(clap::Args, Clone, Debug)] pub(crate) struct EvologArgs { /// Follow changes from these revisions #[arg( long, short, default_value = "@", value_name = "REVSETS", alias = "revision", add = ArgValueCompleter::new(complete::revset_expression_all), )] revisions: Vec, /// Limit number of revisions to show /// /// Applied after revisions are reordered topologically, but before being /// reversed. #[arg(long, short = 'n')] limit: Option, /// Show revisions in the opposite order (older revisions first) #[arg(long)] reversed: bool, /// Don't show the graph, show a flat list of revisions #[arg(long)] no_graph: bool, /// Render each revision using the given template /// /// Run `jj log -T` to list the built-in templates. /// /// You can also specify arbitrary template expressions using the /// [built-in keywords]. See [`jj help -k templates`] for more /// information. /// /// [built-in keywords]: /// https://jj-vcs.github.io/jj/latest/templates/#commit-keywords /// /// [`jj help -k templates`]: /// https://jj-vcs.github.io/jj/latest/templates/ #[arg(long, short = 'T', add = ArgValueCandidates::new(complete::template_aliases))] template: Option, /// Show patch compared to the previous version of this change /// /// 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, } #[instrument(skip_all)] pub(crate) fn cmd_evolog( ui: &mut Ui, command: &CommandHelper, args: &EvologArgs, ) -> Result<(), CommandError> { let workspace_command = command.workspace_helper(ui)?; let start_commit_ids: Vec<_> = workspace_command .parse_union_revsets(ui, &args.revisions)? .evaluate_to_commit_ids()? .try_collect()?; let diff_renderer = workspace_command.diff_renderer_for_log(&args.diff_format, args.patch)?; let graph_style = GraphStyle::from_settings(workspace_command.settings())?; let with_content_format = LogContentFormat::new(ui, workspace_command.settings())?; let template: TemplateRenderer; let node_template: TemplateRenderer>; { let language = workspace_command.commit_template_language(); let template_string = match &args.template { Some(value) => value.to_string(), None => workspace_command.settings().get_string("templates.log")?, }; template = workspace_command .parse_template(ui, &language, &template_string)? .labeled(["log", "commit"]); node_template = workspace_command .parse_template( ui, &language, &workspace_command .settings() .get_string("templates.log_node")?, )? .labeled(["log", "commit", "node"]); } // TODO: better styling and --template argument support let op_summary_template = workspace_command .operation_summary_template() .labeled(["log"]); ui.request_pager(); let mut formatter = ui.stdout_formatter(); let formatter = formatter.as_mut(); let repo = workspace_command.repo(); let evolution_entries = walk_predecessors(repo, &start_commit_ids); if !args.no_graph { let mut raw_output = formatter.raw()?; let mut graph = get_graphlog(graph_style, raw_output.as_mut()); let evolution_nodes = evolution_entries.map_ok(|entry| { let ids = entry.predecessor_ids(); let edges = ids.iter().cloned().map(GraphEdge::direct).collect_vec(); (entry, edges) }); // TopoGroupedGraphIterator also helps emit squashed commits in reverse // chronological order. Predecessors don't need to follow any defined // order. However in practice, if there are multiple predecessors, then // usually the first predecessor is the previous version of the same // change, and the other predecessors are commits that were squashed // into it. If multiple commits are squashed at once, then they are // usually recorded in chronological order. We want to show squashed // commits in reverse chronological order, and we also want to show // squashed commits before the squash destination (since the // destination's subgraph may contain earlier squashed commits as well. let evolution_nodes = TopoGroupedGraphIterator::new(evolution_nodes, |node| node.commit.id()); let evolution_nodes = evolution_nodes.take(args.limit.unwrap_or(usize::MAX)); let evolution_nodes: Box> = if args.reversed { let nodes = reverse_graph(evolution_nodes, |entry| entry.commit.id())?; Box::new(nodes.into_iter().map(Ok)) } else { Box::new(evolution_nodes) }; for node in evolution_nodes { let (entry, edges) = node?; let mut buffer = vec![]; let within_graph = with_content_format.sub_width(graph.width(entry.commit.id(), &edges)); within_graph.write(ui.new_formatter(&mut buffer).as_mut(), |formatter| { template.format(&entry.commit, formatter)?; if let Some(op) = &entry.operation { write!(formatter.labeled("separator"), "--")?; write!(formatter, " operation ")?; op_summary_template.format(op, formatter)?; writeln!(formatter)?; } io::Result::Ok(()) })?; if !buffer.ends_with(b"\n") { buffer.push(b'\n'); } if let Some(renderer) = &diff_renderer { let predecessors: Vec<_> = entry.predecessors().try_collect()?; let mut formatter = ui.new_formatter(&mut buffer); renderer.show_inter_diff( ui, formatter.as_mut(), &predecessors, &entry.commit, &EverythingMatcher, within_graph.width(), )?; } let node_symbol = format_template(ui, &Some(entry.commit.clone()), &node_template); graph.add_node( entry.commit.id(), &edges, &node_symbol, &String::from_utf8_lossy(&buffer), )?; } } else { let evolution_entries = evolution_entries.take(args.limit.unwrap_or(usize::MAX)); let evolution_entries: Box> = if args.reversed { let entries: Vec<_> = evolution_entries.try_collect()?; Box::new(entries.into_iter().rev().map(Ok)) } else { Box::new(evolution_entries) }; for entry in evolution_entries { let entry = entry?; with_content_format.write(formatter, |formatter| { template.format(&entry.commit, formatter)?; if let Some(op) = &entry.operation { write!(formatter, "-- operation ")?; op_summary_template.format(op, formatter)?; writeln!(formatter)?; } io::Result::Ok(()) })?; if let Some(renderer) = &diff_renderer { let predecessors: Vec<_> = entry.predecessors().try_collect()?; let width = ui.term_width(); renderer.show_inter_diff( ui, formatter, &predecessors, &entry.commit, &EverythingMatcher, width, )?; } } } Ok(()) }