jj/cli/src/commands/absorb.rs
2025-05-05 02:54:51 +00:00

143 lines
5.0 KiB
Rust

// 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 clap_complete::ArgValueCompleter;
use jj_lib::absorb::absorb_hunks;
use jj_lib::absorb::split_hunks_to_trees;
use jj_lib::absorb::AbsorbSource;
use jj_lib::matchers::EverythingMatcher;
use pollster::FutureExt as _;
use tracing::instrument;
use crate::cli_util::print_updated_commits;
use crate::cli_util::CommandHelper;
use crate::cli_util::RevisionArg;
use crate::command_error::CommandError;
use crate::complete;
use crate::diff_util::DiffFormat;
use crate::ui::Ui;
/// Move changes from a revision into the stack of mutable revisions
///
/// This command splits changes in the source revision and moves each change to
/// the closest mutable ancestor where the corresponding lines were modified
/// last. If the destination revision cannot be determined unambiguously, the
/// change will be left in the source revision.
///
/// The source revision will be abandoned if all changes are absorbed into the
/// destination revisions, and if the source revision has no description.
///
/// The modification made by `jj absorb` can be reviewed by `jj op show -p`.
#[derive(clap::Args, Clone, Debug)]
pub(crate) struct AbsorbArgs {
/// Source revision to absorb from
#[arg(
long, short,
default_value = "@",
value_name = "REVSET",
add = ArgValueCompleter::new(complete::revset_expression_mutable),
)]
from: RevisionArg,
/// Destination revisions to absorb into
///
/// Only ancestors of the source revision will be considered.
#[arg(
long, short = 't', visible_alias = "to",
default_value = "mutable()",
value_name = "REVSETS",
add = ArgValueCompleter::new(complete::revset_expression_mutable),
)]
into: Vec<RevisionArg>,
/// Move only changes to these paths (instead of all paths)
#[arg(value_name = "FILESETS", value_hint = clap::ValueHint::AnyPath)]
paths: Vec<String>,
}
#[instrument(skip_all)]
pub(crate) fn cmd_absorb(
ui: &mut Ui,
command: &CommandHelper,
args: &AbsorbArgs,
) -> Result<(), CommandError> {
let mut workspace_command = command.workspace_helper(ui)?;
let source_commit = workspace_command.resolve_single_rev(ui, &args.from)?;
let destinations = workspace_command
.parse_union_revsets(ui, &args.into)?
.resolve()?;
let matcher = workspace_command
.parse_file_patterns(ui, &args.paths)?
.to_matcher();
let repo = workspace_command.repo().as_ref();
let source = AbsorbSource::from_commit(repo, source_commit)?;
let selected_trees = split_hunks_to_trees(repo, &source, &destinations, &matcher).block_on()?;
let path_converter = workspace_command.path_converter();
for (path, reason) in selected_trees.skipped_paths {
let ui_path = path_converter.format_file_path(&path);
writeln!(ui.warning_default(), "Skipping {ui_path}: {reason}")?;
}
workspace_command.check_rewritable(selected_trees.target_commits.keys())?;
let mut tx = workspace_command.start_transaction();
let stats = absorb_hunks(tx.repo_mut(), &source, selected_trees.target_commits)?;
if let Some(mut formatter) = ui.status_formatter() {
if !stats.rewritten_destinations.is_empty() {
writeln!(
formatter,
"Absorbed changes into {} revisions:",
stats.rewritten_destinations.len()
)?;
print_updated_commits(
formatter.as_mut(),
&tx.commit_summary_template(),
stats.rewritten_destinations.iter().rev(),
)?;
}
if stats.num_rebased > 0 {
writeln!(
formatter,
"Rebased {} descendant commits.",
stats.num_rebased
)?;
}
}
tx.finish(
ui,
format!(
"absorb changes into {} commits",
stats.rewritten_destinations.len()
),
)?;
if let Some(mut formatter) = ui.status_formatter() {
if let Some(commit) = &stats.rewritten_source {
let repo = workspace_command.repo().as_ref();
if !commit.is_empty(repo)? {
writeln!(formatter, "Remaining changes:")?;
let diff_renderer = workspace_command.diff_renderer(vec![DiffFormat::Summary]);
let matcher = &EverythingMatcher; // also print excluded paths
let width = ui.term_width();
diff_renderer.show_patch(ui, formatter.as_mut(), commit, matcher, width)?;
}
}
}
Ok(())
}