use std::cmp::min;
use clap_complete::ArgValueCandidates;
use clap_complete::ArgValueCompleter;
use futures::StreamExt as _;
use futures::TryStreamExt as _;
use futures::stream;
use futures::stream::LocalBoxStream;
use itertools::Itertools as _;
use jj_lib::backend::CommitId;
use jj_lib::commit::Commit;
use jj_lib::graph::GraphEdge;
use jj_lib::graph::GraphEdgeType;
use jj_lib::graph::TopoGroupedGraph;
use jj_lib::graph::reverse_graph;
use jj_lib::repo::Repo as _;
use jj_lib::revset::RevsetEvaluationError;
use jj_lib::revset::RevsetExpression;
use jj_lib::revset::RevsetFilterPredicate;
use jj_lib::revset::RevsetStreamExt as _;
use pollster::FutureExt as _;
use tracing::instrument;
use crate::cli_util::CommandHelper;
use crate::cli_util::LogContentFormat;
use crate::cli_util::RevisionArg;
use crate::cli_util::format_template;
use crate::command_error::CommandError;
use crate::complete;
use crate::diff_util::DiffFormatArgs;
use crate::formatter::FormatterExt as _;
use crate::graphlog::GraphStyle;
use crate::graphlog::get_graphlog;
use crate::templater::TemplateRenderer;
use crate::ui::Ui;
#[derive(clap::Args, Clone, Debug)]
pub(crate) struct LogArgs {
#[arg(long = "revision", short, value_name = "REVSETS", alias = "revisions")]
#[arg(add = ArgValueCompleter::new(complete::revset_expression_all))]
revisions: Vec<RevisionArg>,
#[arg(value_name = "FILESETS", value_hint = clap::ValueHint::AnyPath)]
#[arg(add = ArgValueCompleter::new(complete::log_files))]
paths: Vec<String>,
#[arg(long, short = 'n')]
limit: Option<usize>,
#[arg(long)]
reversed: bool,
#[arg(long, short = 'G')]
no_graph: bool,
#[arg(long, short = 'T')]
#[arg(add = ArgValueCandidates::new(complete::template_aliases))]
template: Option<String>,
#[arg(long, short = 'p')]
patch: bool,
#[arg(long, conflicts_with_all = ["DiffFormatArgs", "no_graph", "patch", "reversed", "template"])]
count: bool,
#[command(flatten)]
diff_format: DiffFormatArgs,
}
#[instrument(skip_all)]
pub(crate) async fn cmd_log(
ui: &mut Ui,
command: &CommandHelper,
args: &LogArgs,
) -> Result<(), CommandError> {
let workspace_command = command.workspace_helper(ui)?;
let settings = workspace_command.settings();
let fileset_expression = workspace_command.parse_file_patterns(ui, &args.paths)?;
let mut explicit_paths = fileset_expression.explicit_paths().collect_vec();
let revset_expression = {
let mut expression = if args.revisions.is_empty() && args.paths.is_empty() {
let revset_string = settings.get_string("revsets.log")?;
workspace_command.parse_revset(ui, &RevisionArg::from(revset_string))?
} else if !args.revisions.is_empty() {
workspace_command.parse_union_revsets(ui, &args.revisions)?
} else {
workspace_command.attach_revset_evaluator(RevsetExpression::all())
};
if !args.paths.is_empty() {
let predicate = RevsetFilterPredicate::File(fileset_expression.clone());
expression.intersect_with(&RevsetExpression::filter(predicate));
}
expression
};
let revset = revset_expression.evaluate()?;
if args.count {
let (lower, upper) = revset.count_estimate()?;
let limit = args.limit.unwrap_or(usize::MAX);
let count = if limit <= lower {
limit
} else if upper == Some(lower) {
min(lower, limit)
} else {
revset
.stream()
.take(limit)
.try_fold(0, |count, _| async move { Ok(count + 1) })
.await?
};
let mut formatter = ui.stdout_formatter();
writeln!(formatter, "{count}")?;
return Ok(());
}
let prio_revset = settings.get_string("revsets.log-graph-prioritize")?;
let mut prio_revset = workspace_command.parse_revset(ui, &RevisionArg::from(prio_revset))?;
prio_revset.intersect_with(revset_expression.expression());
let repo = workspace_command.repo();
let matcher = fileset_expression.to_matcher();
let store = repo.store();
let diff_renderer = workspace_command.diff_renderer_for_log(&args.diff_format, args.patch)?;
let graph_style = GraphStyle::from_settings(settings)?;
let use_elided_nodes = settings.get_bool("ui.log-synthetic-elided-nodes")?;
let with_content_format = LogContentFormat::new(ui, settings)?;
let template: TemplateRenderer<Commit>;
let node_template: TemplateRenderer<Option<Commit>>;
{
let language = workspace_command.commit_template_language();
let template_string = match &args.template {
Some(value) => value.clone(),
None => 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, &settings.get_string("templates.log_node")?)?
.labeled(["log", "commit", "node"]);
}
{
ui.request_pager();
let mut formatter = ui.stdout_formatter();
let formatter = formatter.as_mut();
if !args.no_graph {
let mut raw_output = formatter.raw()?;
let mut graph = get_graphlog(graph_style, raw_output.as_mut());
let mut stream: LocalBoxStream<_> = {
let mut topo_order = TopoGroupedGraph::new(revset.stream_graph(), |id| id);
let mut prio_stream = prio_revset.evaluate_to_commit_ids()?;
while let Some(prio) = prio_stream.try_next().await? {
topo_order.prioritize_branch(prio);
}
let forward_stream = topo_order.stream();
let forward_stream = forward_stream.take(args.limit.unwrap_or(usize::MAX));
if args.reversed {
let nodes: Vec<_> = forward_stream.collect().await;
let nodes = reverse_graph(nodes.into_iter(), |id: &CommitId| id)?;
stream::iter(nodes.into_iter().map(Ok)).boxed_local()
} else {
forward_stream.boxed_local()
}
};
while let Some((commit_id, edges)) = stream.try_next().await? {
let mut graphlog_edges = vec![];
let mut missing_edge_id = None;
let mut elided_targets = vec![];
for edge in edges {
match edge.edge_type {
GraphEdgeType::Missing => {
missing_edge_id = Some(edge.target);
}
GraphEdgeType::Direct => {
graphlog_edges.push(GraphEdge::direct((edge.target, false)));
}
GraphEdgeType::Indirect => {
if use_elided_nodes {
elided_targets.push(edge.target.clone());
graphlog_edges.push(GraphEdge::direct((edge.target, true)));
} else {
graphlog_edges.push(GraphEdge::indirect((edge.target, false)));
}
}
}
}
if let Some(missing_edge_id) = missing_edge_id {
graphlog_edges.push(GraphEdge::missing((missing_edge_id, false)));
}
let mut buffer = vec![];
let key = (commit_id, false);
let commit = store.get_commit_async(&key.0).await?;
let within_graph =
with_content_format.sub_width(graph.width(&key, &graphlog_edges));
within_graph
.write(ui.new_formatter(&mut buffer).as_mut(), async |formatter| {
template.format(&commit, formatter)
})
.await?;
if let Some(renderer) = &diff_renderer {
let mut formatter = ui.new_formatter(&mut buffer);
renderer
.show_patch(
ui,
formatter.as_mut(),
&commit,
matcher.as_ref(),
within_graph.width(),
)
.await?;
}
let commit = Some(commit);
let node_symbol = format_template(ui, &commit, &node_template);
graph.add_node(
&key,
&graphlog_edges,
&node_symbol,
&String::from_utf8_lossy(&buffer),
)?;
let tree = commit.map(|c| c.tree()).unwrap();
explicit_paths
.retain(|&path| tree.path_value(path).block_on().unwrap().is_absent());
for elided_target in elided_targets {
let elided_key = (elided_target, true);
let real_key = (elided_key.0.clone(), false);
let edges = [GraphEdge::direct(real_key)];
let mut buffer = vec![];
let within_graph =
with_content_format.sub_width(graph.width(&elided_key, &edges));
within_graph
.write(ui.new_formatter(&mut buffer).as_mut(), async |formatter| {
writeln!(formatter.labeled("elided"), "(elided revisions)")
})
.await?;
let node_symbol = format_template(ui, &None, &node_template);
graph.add_node(
&elided_key,
&edges,
&node_symbol,
&String::from_utf8_lossy(&buffer),
)?;
}
}
} else {
let id_stream: LocalBoxStream<Result<CommitId, RevsetEvaluationError>> = {
let forward_stream = revset.stream().take(args.limit.unwrap_or(usize::MAX));
if args.reversed {
let entries: Vec<_> = forward_stream.try_collect().await?;
stream::iter(entries.into_iter().rev().map(Ok)).boxed_local()
} else {
forward_stream.boxed_local()
}
};
let mut commit_stream = id_stream.commits(store);
while let Some(commit) = commit_stream.try_next().await? {
with_content_format
.write(formatter, async |formatter| {
template.format(&commit, formatter)
})
.await?;
if let Some(renderer) = &diff_renderer {
let width = ui.term_width();
renderer
.show_patch(ui, formatter, &commit, matcher.as_ref(), width)
.await?;
}
let tree = commit.tree();
explicit_paths
.retain(|&path| tree.path_value(path).block_on().unwrap().is_absent());
}
}
if !explicit_paths.is_empty() {
let ui_paths = explicit_paths
.iter()
.map(|&path| workspace_command.format_file_path(path))
.join(", ");
writeln!(
ui.warning_default(),
"No matching entries for paths: {ui_paths}"
)?;
}
}
if let ([], [only_path]) = (args.revisions.as_slice(), args.paths.as_slice()) {
if only_path == "." && workspace_command.parse_file_path(only_path)?.is_root() {
writeln!(
ui.warning_default(),
"The argument {only_path:?} is being interpreted as a fileset expression, but \
this is often not useful because all non-empty commits touch '.'. If you meant \
to show the working copy commit, pass -r '@' instead."
)?;
} else if revset.is_empty()
&& workspace_command
.parse_revset(ui, &RevisionArg::from(only_path.to_owned()))
.is_ok()
{
writeln!(
ui.warning_default(),
"The argument {only_path:?} is being interpreted as a fileset expression. To \
specify a revset, pass -r {only_path:?} instead."
)?;
}
}
Ok(())
}