commands: move split code to split.rs

This commit is contained in:
Antoine Cezar 2023-11-02 19:19:27 +01:00 committed by Antoine Cezar
parent 6d0633443b
commit 38898336a0
2 changed files with 193 additions and 160 deletions

View file

@ -46,6 +46,7 @@ mod restore;
mod run;
mod show;
mod sparse;
mod split;
use std::fmt::Debug;
use std::io::Write;
@ -61,12 +62,11 @@ use jj_lib::merge::Merge;
use jj_lib::merged_tree::{MergedTree, MergedTreeBuilder};
use jj_lib::op_store::WorkspaceId;
use jj_lib::repo::{ReadonlyRepo, Repo};
use jj_lib::rewrite::{merge_commit_trees, DescendantRebaser};
use jj_lib::rewrite::merge_commit_trees;
use jj_lib::settings::UserSettings;
use jj_lib::working_copy::SnapshotOptions;
use jj_lib::workspace::{default_working_copy_initializer, Workspace};
use jj_lib::{file_util, revset};
use maplit::{hashmap, hashset};
use tracing::instrument;
use crate::cli_util::{
@ -134,7 +134,7 @@ enum Commands {
Show(show::ShowArgs),
#[command(subcommand)]
Sparse(sparse::SparseArgs),
Split(SplitArgs),
Split(split::SplitArgs),
Squash(SquashArgs),
Status(StatusArgs),
#[command(subcommand)]
@ -220,32 +220,6 @@ struct UnsquashArgs {
interactive: bool,
}
/// Split a revision in two
///
/// Starts a diff editor (`meld` by default) on the changes in the revision.
/// Edit the right side of the diff until it has the content you want in the
/// first revision. Once you close the editor, your edited content will replace
/// the previous revision. The remaining changes will be put in a new revision
/// on top.
///
/// If the change you split had a description, you will be asked to enter a
/// change description for each commit. If the change did not have a
/// description, the second part will not get a description, and you will be
/// asked for a description only for the first part.
#[derive(clap::Args, Clone, Debug)]
struct SplitArgs {
/// Interactively choose which parts to split. This is the default if no
/// paths are provided.
#[arg(long, short)]
interactive: bool,
/// The revision to split
#[arg(long, short, default_value = "@")]
revision: RevisionArg,
/// Put these paths in the first commit
#[arg(value_hint = clap::ValueHint::AnyPath)]
paths: Vec<String>,
}
/// Commands for working with workspaces
///
/// Workspaces let you add additional working copies attached to the same repo.
@ -845,33 +819,6 @@ fn description_template_for_commit(
}
}
fn description_template_for_cmd_split(
ui: &Ui,
settings: &UserSettings,
workspace_command: &WorkspaceCommandHelper,
intro: &str,
overall_commit_description: &str,
from_tree: &MergedTree,
to_tree: &MergedTree,
) -> Result<String, CommandError> {
let mut diff_summary_bytes = Vec::new();
diff_util::show_diff(
ui,
&mut PlainTextFormatter::new(&mut diff_summary_bytes),
workspace_command,
from_tree,
to_tree,
&EverythingMatcher,
&[DiffFormat::Summary],
)?;
let description = if overall_commit_description.is_empty() {
settings.default_description()
} else {
overall_commit_description.to_owned()
};
Ok(format!("JJ: {intro}\n{description}\n") + &diff_summary_to_description(&diff_summary_bytes))
}
fn diff_summary_to_description(bytes: &[u8]) -> String {
let text = std::str::from_utf8(bytes).expect(
"Summary diffs and repo paths must always be valid UTF8.",
@ -881,109 +828,6 @@ fn diff_summary_to_description(bytes: &[u8]) -> String {
+ &textwrap::indent(text, "JJ: ")
}
#[instrument(skip_all)]
fn cmd_split(ui: &mut Ui, command: &CommandHelper, args: &SplitArgs) -> Result<(), CommandError> {
let mut workspace_command = command.workspace_helper(ui)?;
let commit = workspace_command.resolve_single_rev(&args.revision, ui)?;
workspace_command.check_rewritable([&commit])?;
let matcher = workspace_command.matcher_from_values(&args.paths)?;
let mut tx =
workspace_command.start_transaction(&format!("split commit {}", commit.id().hex()));
let end_tree = commit.tree()?;
let base_tree = merge_commit_trees(tx.repo(), &commit.parents())?;
let interactive = args.interactive || args.paths.is_empty();
let instructions = format!(
"\
You are splitting a commit in two: {}
The diff initially shows the changes in the commit you're splitting.
Adjust the right side until it shows the contents you want for the first
(parent) commit. The remainder will be in the second commit. If you
don't make any changes, then the operation will be aborted.
",
tx.format_commit_summary(&commit)
);
let tree_id = tx.select_diff(
ui,
&base_tree,
&end_tree,
matcher.as_ref(),
&instructions,
interactive,
)?;
if &tree_id == commit.tree_id() && interactive {
writeln!(ui.stderr(), "Nothing changed.")?;
return Ok(());
}
let middle_tree = tx.repo().store().get_root_tree(&tree_id)?;
if middle_tree.id() == base_tree.id() {
writeln!(
ui.warning(),
"The given paths do not match any file: {}",
args.paths.join(" ")
)?;
}
let first_template = description_template_for_cmd_split(
ui,
command.settings(),
tx.base_workspace_helper(),
"Enter commit description for the first part (parent).",
commit.description(),
&base_tree,
&middle_tree,
)?;
let first_description = edit_description(tx.base_repo(), &first_template, command.settings())?;
let first_commit = tx
.mut_repo()
.rewrite_commit(command.settings(), &commit)
.set_tree_id(tree_id)
.set_description(first_description)
.write()?;
let second_description = if commit.description().is_empty() {
// If there was no description before, don't ask for one for the second commit.
"".to_string()
} else {
let second_template = description_template_for_cmd_split(
ui,
command.settings(),
tx.base_workspace_helper(),
"Enter commit description for the second part (child).",
commit.description(),
&middle_tree,
&end_tree,
)?;
edit_description(tx.base_repo(), &second_template, command.settings())?
};
let second_commit = tx
.mut_repo()
.rewrite_commit(command.settings(), &commit)
.set_parents(vec![first_commit.id().clone()])
.set_tree_id(commit.tree_id().clone())
.generate_new_change_id()
.set_description(second_description)
.write()?;
let mut rebaser = DescendantRebaser::new(
command.settings(),
tx.mut_repo(),
hashmap! { commit.id().clone() => hashset!{second_commit.id().clone()} },
hashset! {},
);
rebaser.rebase_all()?;
let num_rebased = rebaser.rebased().len();
if num_rebased > 0 {
writeln!(ui.stderr(), "Rebased {num_rebased} descendant commits")?;
}
write!(ui.stderr(), "First part: ")?;
tx.write_commit_summary(ui.stderr_formatter().as_mut(), &first_commit)?;
write!(ui.stderr(), "\nSecond part: ")?;
tx.write_commit_summary(ui.stderr_formatter().as_mut(), &second_commit)?;
writeln!(ui.stderr())?;
tx.finish(ui)?;
Ok(())
}
fn make_branch_term(branch_names: &[impl fmt::Display]) -> String {
match branch_names {
[branch_name] => format!("branch {}", branch_name),
@ -1304,7 +1148,7 @@ pub fn run_command(ui: &mut Ui, command_helper: &CommandHelper) -> Result<(), Co
Commands::Restore(sub_args) => restore::cmd_restore(ui, command_helper, sub_args),
Commands::Run(sub_args) => run::cmd_run(ui, command_helper, sub_args),
Commands::Diffedit(sub_args) => diffedit::cmd_diffedit(ui, command_helper, sub_args),
Commands::Split(sub_args) => cmd_split(ui, command_helper, sub_args),
Commands::Split(sub_args) => split::cmd_split(ui, command_helper, sub_args),
Commands::Merge(sub_args) => merge::cmd_merge(ui, command_helper, sub_args),
Commands::Rebase(sub_args) => rebase::cmd_rebase(ui, command_helper, sub_args),
Commands::Backout(sub_args) => backout::cmd_backout(ui, command_helper, sub_args),

189
cli/src/commands/split.rs Normal file
View file

@ -0,0 +1,189 @@
// 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::Write;
use jj_lib::backend::ObjectId;
use jj_lib::matchers::EverythingMatcher;
use jj_lib::merged_tree::MergedTree;
use jj_lib::repo::Repo;
use jj_lib::rewrite::{merge_commit_trees, DescendantRebaser};
use jj_lib::settings::UserSettings;
use maplit::{hashmap, hashset};
use tracing::instrument;
use super::{diff_summary_to_description, edit_description};
use crate::cli_util::{CommandError, CommandHelper, RevisionArg, WorkspaceCommandHelper};
use crate::diff_util::{self, DiffFormat};
use crate::formatter::PlainTextFormatter;
use crate::ui::Ui;
/// Split a revision in two
///
/// Starts a diff editor (`meld` by default) on the changes in the revision.
/// Edit the right side of the diff until it has the content you want in the
/// first revision. Once you close the editor, your edited content will replace
/// the previous revision. The remaining changes will be put in a new revision
/// on top.
///
/// If the change you split had a description, you will be asked to enter a
/// change description for each commit. If the change did not have a
/// description, the second part will not get a description, and you will be
/// asked for a description only for the first part.
#[derive(clap::Args, Clone, Debug)]
pub(crate) struct SplitArgs {
/// Interactively choose which parts to split. This is the default if no
/// paths are provided.
#[arg(long, short)]
interactive: bool,
/// The revision to split
#[arg(long, short, default_value = "@")]
revision: RevisionArg,
/// Put these paths in the first commit
#[arg(value_hint = clap::ValueHint::AnyPath)]
paths: Vec<String>,
}
#[instrument(skip_all)]
pub(crate) fn cmd_split(
ui: &mut Ui,
command: &CommandHelper,
args: &SplitArgs,
) -> Result<(), CommandError> {
let mut workspace_command = command.workspace_helper(ui)?;
let commit = workspace_command.resolve_single_rev(&args.revision, ui)?;
workspace_command.check_rewritable([&commit])?;
let matcher = workspace_command.matcher_from_values(&args.paths)?;
let mut tx =
workspace_command.start_transaction(&format!("split commit {}", commit.id().hex()));
let end_tree = commit.tree()?;
let base_tree = merge_commit_trees(tx.repo(), &commit.parents())?;
let interactive = args.interactive || args.paths.is_empty();
let instructions = format!(
"\
You are splitting a commit in two: {}
The diff initially shows the changes in the commit you're splitting.
Adjust the right side until it shows the contents you want for the first
(parent) commit. The remainder will be in the second commit. If you
don't make any changes, then the operation will be aborted.
",
tx.format_commit_summary(&commit)
);
let tree_id = tx.select_diff(
ui,
&base_tree,
&end_tree,
matcher.as_ref(),
&instructions,
interactive,
)?;
if &tree_id == commit.tree_id() && interactive {
writeln!(ui.stderr(), "Nothing changed.")?;
return Ok(());
}
let middle_tree = tx.repo().store().get_root_tree(&tree_id)?;
if middle_tree.id() == base_tree.id() {
writeln!(
ui.warning(),
"The given paths do not match any file: {}",
args.paths.join(" ")
)?;
}
let first_template = description_template_for_cmd_split(
ui,
command.settings(),
tx.base_workspace_helper(),
"Enter commit description for the first part (parent).",
commit.description(),
&base_tree,
&middle_tree,
)?;
let first_description = edit_description(tx.base_repo(), &first_template, command.settings())?;
let first_commit = tx
.mut_repo()
.rewrite_commit(command.settings(), &commit)
.set_tree_id(tree_id)
.set_description(first_description)
.write()?;
let second_description = if commit.description().is_empty() {
// If there was no description before, don't ask for one for the second commit.
"".to_string()
} else {
let second_template = description_template_for_cmd_split(
ui,
command.settings(),
tx.base_workspace_helper(),
"Enter commit description for the second part (child).",
commit.description(),
&middle_tree,
&end_tree,
)?;
edit_description(tx.base_repo(), &second_template, command.settings())?
};
let second_commit = tx
.mut_repo()
.rewrite_commit(command.settings(), &commit)
.set_parents(vec![first_commit.id().clone()])
.set_tree_id(commit.tree_id().clone())
.generate_new_change_id()
.set_description(second_description)
.write()?;
let mut rebaser = DescendantRebaser::new(
command.settings(),
tx.mut_repo(),
hashmap! { commit.id().clone() => hashset!{second_commit.id().clone()} },
hashset! {},
);
rebaser.rebase_all()?;
let num_rebased = rebaser.rebased().len();
if num_rebased > 0 {
writeln!(ui.stderr(), "Rebased {num_rebased} descendant commits")?;
}
write!(ui.stderr(), "First part: ")?;
tx.write_commit_summary(ui.stderr_formatter().as_mut(), &first_commit)?;
write!(ui.stderr(), "\nSecond part: ")?;
tx.write_commit_summary(ui.stderr_formatter().as_mut(), &second_commit)?;
writeln!(ui.stderr())?;
tx.finish(ui)?;
Ok(())
}
fn description_template_for_cmd_split(
ui: &Ui,
settings: &UserSettings,
workspace_command: &WorkspaceCommandHelper,
intro: &str,
overall_commit_description: &str,
from_tree: &MergedTree,
to_tree: &MergedTree,
) -> Result<String, CommandError> {
let mut diff_summary_bytes = Vec::new();
diff_util::show_diff(
ui,
&mut PlainTextFormatter::new(&mut diff_summary_bytes),
workspace_command,
from_tree,
to_tree,
&EverythingMatcher,
&[DiffFormat::Summary],
)?;
let description = if overall_commit_description.is_empty() {
settings.default_description()
} else {
overall_commit_description.to_owned()
};
Ok(format!("JJ: {intro}\n{description}\n") + &diff_summary_to_description(&diff_summary_bytes))
}