// 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 itertools::Itertools as _;use jj_lib::copies::CopyRecords;use jj_lib::repo::Repo as _;use jj_lib::revset::RevsetExpression;use jj_lib::revset::RevsetFilterPredicate;use tracing::instrument; use crate::cli_util::print_conflicted_paths;use crate::cli_util::print_snapshot_stats;use crate::cli_util::CommandHelper;use crate::command_error::CommandError;use crate::diff_util::get_copy_records;use crate::diff_util::DiffFormat;use crate::ui::Ui; /// Show high-level repo status////// This includes:////// * The working copy commit and its (first) parent, and a summary of the/// changes between them/// * [Conflicted bookmarks]////// [Conflicted bookmarks]:/// https://jj-vcs.github.io/jj/latest/bookmarks/#conflicts#[derive(clap::Args, Clone, Debug)]#[command(visible_alias = "st")]pub(crate) struct StatusArgs { /// Restrict the status display to these paths #[arg(value_name = "FILESETS", value_hint = clap::ValueHint::AnyPath)] paths: Vec<String>,} #[instrument(skip_all)]pub(crate) fn cmd_status( ui: &mut Ui, command: &CommandHelper, args: &StatusArgs,) -> Result<(), CommandError> { let (workspace_command, snapshot_stats) = command.workspace_helper_with_stats(ui)?; print_snapshot_stats( ui, &snapshot_stats, workspace_command.env().path_converter(), )?; let repo = workspace_command.repo(); let maybe_wc_commit = workspace_command .get_wc_commit_id() .map(|id| repo.store().get_commit(id)) .transpose()?; let matcher = workspace_command .parse_file_patterns(ui, &args.paths)? .to_matcher(); ui.request_pager(); let mut formatter = ui.stdout_formatter(); let formatter = formatter.as_mut(); if let Some(wc_commit) = &maybe_wc_commit { let parent_tree = wc_commit.parent_tree(repo.as_ref())?; let tree = wc_commit.tree()?; let wc_has_changes = tree.id() != parent_tree.id(); let wc_has_untracked = !snapshot_stats.untracked_paths.is_empty(); if !wc_has_changes && !wc_has_untracked { writeln!(formatter, "The working copy has no changes.")?; } else { if wc_has_changes { writeln!(formatter, "Working copy changes:")?; let mut copy_records = CopyRecords::default(); for parent in wc_commit.parent_ids() { let records = get_copy_records(repo.store(), parent, wc_commit.id(), &matcher)?; copy_records.add_records(records)?; } let diff_renderer = workspace_command.diff_renderer(vec![DiffFormat::Summary]); let width = ui.term_width(); diff_renderer.show_diff( ui, formatter, &parent_tree, &tree, &matcher, ©_records, width, )?; } if wc_has_untracked { writeln!(formatter, "Untracked paths:")?; formatter.with_label("diff", |formatter| { for path in snapshot_stats.untracked_paths.keys() { let ui_path = workspace_command.path_converter().format_file_path(path); writeln!(formatter.labeled("untracked"), "? {ui_path}")?; } io::Result::Ok(()) })?; } } let template = workspace_command.commit_summary_template(); write!(formatter, "Working copy (@) : ")?; formatter.with_label("working_copy", |fmt| template.format(wc_commit, fmt))?; writeln!(formatter)?; for parent in wc_commit.parents() { let parent = parent?; // "Working copy (@) : " write!(formatter, "Parent commit (@-): ")?; template.format(&parent, formatter)?; writeln!(formatter)?; } if wc_commit.has_conflict()? { // TODO: Conflicts should also be filtered by the `matcher`. See the related // TODO on `MergedTree::conflicts()`. let conflicts = wc_commit.tree()?.conflicts().collect_vec(); writeln!( formatter.labeled("warning").with_heading("Warning: "), "There are unresolved conflicts at these paths:" )?; print_conflicted_paths(conflicts, formatter, &workspace_command)?; let wc_revset = RevsetExpression::commit(wc_commit.id().clone()); // Ancestors with conflicts, excluding the current working copy commit. let ancestors_conflicts: Vec<_> = workspace_command .attach_revset_evaluator( wc_revset .parents() .ancestors() .filtered(RevsetFilterPredicate::HasConflict) .minus(&workspace_command.env().immutable_expression()), ) .evaluate_to_commit_ids()? .try_collect()?; workspace_command.report_repo_conflicts(formatter, repo, ancestors_conflicts)?; } else { for parent in wc_commit.parents() { let parent = parent?; if parent.has_conflict()? { writeln!( formatter.labeled("hint").with_heading("Hint: "), "Conflict in parent commit has been resolved in working copy" )?; break; } } } } else { writeln!(formatter, "No working copy")?; } let conflicted_local_bookmarks = repo .view() .local_bookmarks() .filter(|(_, target)| target.has_conflict()) .map(|(bookmark_name, _)| bookmark_name) .collect_vec(); let conflicted_remote_bookmarks = repo .view() .all_remote_bookmarks() .filter(|(_, remote_ref)| remote_ref.target.has_conflict()) .map(|(symbol, _)| symbol) .collect_vec(); if !conflicted_local_bookmarks.is_empty() { writeln!( formatter.labeled("warning").with_heading("Warning: "), "These bookmarks have conflicts:" )?; for name in conflicted_local_bookmarks { write!(formatter, " ")?; write!(formatter.labeled("bookmark"), "{}", name.as_symbol())?; writeln!(formatter)?; } writeln!( formatter.labeled("hint").with_heading("Hint: "), "Use `jj bookmark list` to see details. Use `jj bookmark set <name> -r <rev>` to \ resolve." )?; } if !conflicted_remote_bookmarks.is_empty() { writeln!( formatter.labeled("warning").with_heading("Warning: "), "These remote bookmarks have conflicts:" )?; for symbol in conflicted_remote_bookmarks { write!(formatter, " ")?; write!(formatter.labeled("bookmark"), "{symbol}")?; writeln!(formatter)?; } writeln!( formatter.labeled("hint").with_heading("Hint: "), "Use `jj bookmark list` to see details. Use `jj git fetch` to resolve." )?; } Ok(())}
// 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 itertools::Itertools as _;
use jj_lib::copies::CopyRecords;
use jj_lib::repo::Repo as _;
use jj_lib::revset::RevsetExpression;
use jj_lib::revset::RevsetFilterPredicate;
use tracing::instrument;
use crate::cli_util::print_conflicted_paths;
use crate::cli_util::print_snapshot_stats;
use crate::cli_util::CommandHelper;
use crate::command_error::CommandError;
use crate::diff_util::get_copy_records;
use crate::diff_util::DiffFormat;
use crate::ui::Ui;
/// Show high-level repo status
///
/// This includes:
///
/// * The working copy commit and its (first) parent, and a summary of the
/// changes between them
/// * [Conflicted bookmarks]
///
/// [Conflicted bookmarks]:
/// https://jj-vcs.github.io/jj/latest/bookmarks/#conflicts
#[derive(clap::Args, Clone, Debug)]
#[command(visible_alias = "st")]
pub(crate) struct StatusArgs {
/// Restrict the status display to these paths
#[arg(value_name = "FILESETS", value_hint = clap::ValueHint::AnyPath)]
paths: Vec<String>,
}
#[instrument(skip_all)]
pub(crate) fn cmd_status(
ui: &mut Ui,
command: &CommandHelper,
args: &StatusArgs,
) -> Result<(), CommandError> {
let (workspace_command, snapshot_stats) = command.workspace_helper_with_stats(ui)?;
print_snapshot_stats(
ui,
&snapshot_stats,
workspace_command.env().path_converter(),
)?;
let repo = workspace_command.repo();
let maybe_wc_commit = workspace_command
.get_wc_commit_id()
.map(|id| repo.store().get_commit(id))
.transpose()?;
let matcher = workspace_command
.parse_file_patterns(ui, &args.paths)?
.to_matcher();
ui.request_pager();
let mut formatter = ui.stdout_formatter();
let formatter = formatter.as_mut();
if let Some(wc_commit) = &maybe_wc_commit {
let parent_tree = wc_commit.parent_tree(repo.as_ref())?;
let tree = wc_commit.tree()?;
let wc_has_changes = tree.id() != parent_tree.id();
let wc_has_untracked = !snapshot_stats.untracked_paths.is_empty();
if !wc_has_changes && !wc_has_untracked {
writeln!(formatter, "The working copy has no changes.")?;
} else {
if wc_has_changes {
writeln!(formatter, "Working copy changes:")?;
let mut copy_records = CopyRecords::default();
for parent in wc_commit.parent_ids() {
let records = get_copy_records(repo.store(), parent, wc_commit.id(), &matcher)?;
copy_records.add_records(records)?;
}
let diff_renderer = workspace_command.diff_renderer(vec![DiffFormat::Summary]);
let width = ui.term_width();
diff_renderer.show_diff(
ui,
formatter,
&parent_tree,
&tree,
&matcher,
©_records,
width,
)?;
}
if wc_has_untracked {
writeln!(formatter, "Untracked paths:")?;
formatter.with_label("diff", |formatter| {
for path in snapshot_stats.untracked_paths.keys() {
let ui_path = workspace_command.path_converter().format_file_path(path);
writeln!(formatter.labeled("untracked"), "? {ui_path}")?;
}
io::Result::Ok(())
})?;
}
}
let template = workspace_command.commit_summary_template();
write!(formatter, "Working copy (@) : ")?;
formatter.with_label("working_copy", |fmt| template.format(wc_commit, fmt))?;
writeln!(formatter)?;
for parent in wc_commit.parents() {
let parent = parent?;
// "Working copy (@) : "
write!(formatter, "Parent commit (@-): ")?;
template.format(&parent, formatter)?;
writeln!(formatter)?;
}
if wc_commit.has_conflict()? {
// TODO: Conflicts should also be filtered by the `matcher`. See the related
// TODO on `MergedTree::conflicts()`.
let conflicts = wc_commit.tree()?.conflicts().collect_vec();
writeln!(
formatter.labeled("warning").with_heading("Warning: "),
"There are unresolved conflicts at these paths:"
)?;
print_conflicted_paths(conflicts, formatter, &workspace_command)?;
let wc_revset = RevsetExpression::commit(wc_commit.id().clone());
// Ancestors with conflicts, excluding the current working copy commit.
let ancestors_conflicts: Vec<_> = workspace_command
.attach_revset_evaluator(
wc_revset
.parents()
.ancestors()
.filtered(RevsetFilterPredicate::HasConflict)
.minus(&workspace_command.env().immutable_expression()),
)
.evaluate_to_commit_ids()?
.try_collect()?;
workspace_command.report_repo_conflicts(formatter, repo, ancestors_conflicts)?;
} else {
for parent in wc_commit.parents() {
let parent = parent?;
if parent.has_conflict()? {
writeln!(
formatter.labeled("hint").with_heading("Hint: "),
"Conflict in parent commit has been resolved in working copy"
)?;
break;
}
}
}
} else {
writeln!(formatter, "No working copy")?;
}
let conflicted_local_bookmarks = repo
.view()
.local_bookmarks()
.filter(|(_, target)| target.has_conflict())
.map(|(bookmark_name, _)| bookmark_name)
.collect_vec();
let conflicted_remote_bookmarks = repo
.view()
.all_remote_bookmarks()
.filter(|(_, remote_ref)| remote_ref.target.has_conflict())
.map(|(symbol, _)| symbol)
.collect_vec();
if !conflicted_local_bookmarks.is_empty() {
writeln!(
formatter.labeled("warning").with_heading("Warning: "),
"These bookmarks have conflicts:"
)?;
for name in conflicted_local_bookmarks {
write!(formatter, " ")?;
write!(formatter.labeled("bookmark"), "{}", name.as_symbol())?;
writeln!(formatter)?;
}
writeln!(
formatter.labeled("hint").with_heading("Hint: "),
"Use `jj bookmark list` to see details. Use `jj bookmark set <name> -r <rev>` to \
resolve."
)?;
}
if !conflicted_remote_bookmarks.is_empty() {
writeln!(
formatter.labeled("warning").with_heading("Warning: "),
"These remote bookmarks have conflicts:"
)?;
for symbol in conflicted_remote_bookmarks {
write!(formatter, " ")?;
write!(formatter.labeled("bookmark"), "{symbol}")?;
writeln!(formatter)?;
}
writeln!(
formatter.labeled("hint").with_heading("Hint: "),
"Use `jj bookmark list` to see details. Use `jj git fetch` to resolve."
)?;
}
Ok(())
}