diff --git a/cli/src/commands/commit.rs b/cli/src/commands/commit.rs index 8135ea2178..8df907499d 100644 --- a/cli/src/commands/commit.rs +++ b/cli/src/commands/commit.rs @@ -17,8 +17,8 @@ use jj_lib::repo::Repo; use jj_lib::rewrite::merge_commit_trees; use tracing::instrument; -use super::{description_template_for_commit, edit_description}; use crate::cli_util::{join_message_paragraphs, user_error, CommandError, CommandHelper}; +use crate::description_util::{description_template_for_commit, edit_description}; use crate::ui::Ui; /// Update the description and create a new change on top. diff --git a/cli/src/commands/describe.rs b/cli/src/commands/describe.rs index baf699d8a6..d9486cdc3e 100644 --- a/cli/src/commands/describe.rs +++ b/cli/src/commands/describe.rs @@ -17,8 +17,8 @@ use std::io::{self, Read, Write}; use jj_lib::backend::ObjectId; use tracing::instrument; -use super::{description_template_for_commit, edit_description}; use crate::cli_util::{join_message_paragraphs, CommandError, CommandHelper, RevisionArg}; +use crate::description_util::{description_template_for_commit, edit_description}; use crate::ui::Ui; /// Update the change description or other metadata diff --git a/cli/src/commands/mod.rs b/cli/src/commands/mod.rs index 899e46cd3f..2efb68db9c 100644 --- a/cli/src/commands/mod.rs +++ b/cli/src/commands/mod.rs @@ -55,24 +55,14 @@ mod util; mod version; mod workspace; +use std::fmt; use std::fmt::Debug; -use std::io::Write; -use std::{fmt, fs, io}; use clap::{Command, CommandFactory, FromArgMatches, Subcommand}; use itertools::Itertools; -use jj_lib::commit::Commit; -use jj_lib::matchers::EverythingMatcher; -use jj_lib::repo::ReadonlyRepo; -use jj_lib::settings::UserSettings; use tracing::instrument; -use crate::cli_util::{ - run_ui_editor, user_error, Args, CommandError, CommandHelper, WorkspaceCommandHelper, -}; -use crate::diff_util::{self, DiffFormat}; -use crate::formatter::PlainTextFormatter; -use crate::text_util; +use crate::cli_util::{Args, CommandError, CommandHelper}; use crate::ui::Ui; #[derive(clap::Parser, Clone, Debug)] @@ -145,109 +135,6 @@ enum Commands { Workspace(workspace::WorkspaceCommands), } -fn edit_description( - repo: &ReadonlyRepo, - description: &str, - settings: &UserSettings, -) -> Result { - let description_file_path = (|| -> Result<_, io::Error> { - let mut file = tempfile::Builder::new() - .prefix("editor-") - .suffix(".jjdescription") - .tempfile_in(repo.repo_path())?; - file.write_all(description.as_bytes())?; - file.write_all(b"\nJJ: Lines starting with \"JJ: \" (like this one) will be removed.\n")?; - let (_, path) = file.keep().map_err(|e| e.error)?; - Ok(path) - })() - .map_err(|e| { - user_error(format!( - r#"Failed to create description file in "{path}": {e}"#, - path = repo.repo_path().display() - )) - })?; - - run_ui_editor(settings, &description_file_path)?; - - let description = fs::read_to_string(&description_file_path).map_err(|e| { - user_error(format!( - r#"Failed to read description file "{path}": {e}"#, - path = description_file_path.display() - )) - })?; - // Delete the file only if everything went well. - // TODO: Tell the user the name of the file we left behind. - std::fs::remove_file(description_file_path).ok(); - // Normalize line ending, remove leading and trailing blank lines. - let description = description - .lines() - .filter(|line| !line.starts_with("JJ: ")) - .join("\n"); - Ok(text_util::complete_newline(description.trim_matches('\n'))) -} - -fn combine_messages( - repo: &ReadonlyRepo, - source: &Commit, - destination: &Commit, - settings: &UserSettings, - abandon_source: bool, -) -> Result { - let description = if abandon_source { - if source.description().is_empty() { - destination.description().to_string() - } else if destination.description().is_empty() { - source.description().to_string() - } else { - let combined = "JJ: Enter a description for the combined commit.\n".to_string() - + "JJ: Description from the destination commit:\n" - + destination.description() - + "\nJJ: Description from the source commit:\n" - + source.description(); - edit_description(repo, &combined, settings)? - } - } else { - destination.description().to_string() - }; - Ok(description) -} - -fn description_template_for_commit( - ui: &Ui, - settings: &UserSettings, - workspace_command: &WorkspaceCommandHelper, - commit: &Commit, -) -> Result { - let mut diff_summary_bytes = Vec::new(); - diff_util::show_patch( - ui, - &mut PlainTextFormatter::new(&mut diff_summary_bytes), - workspace_command, - commit, - &EverythingMatcher, - &[DiffFormat::Summary], - )?; - let description = if commit.description().is_empty() { - settings.default_description() - } else { - commit.description().to_owned() - }; - if diff_summary_bytes.is_empty() { - Ok(description) - } else { - Ok(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.", - // Double-check this assumption for diffs that include file content. - ); - "JJ: This commit contains the following changes:\n".to_owned() - + &textwrap::indent(text, "JJ: ") -} - fn make_branch_term(branch_names: &[impl fmt::Display]) -> String { match branch_names { [branch_name] => format!("branch {}", branch_name), diff --git a/cli/src/commands/move.rs b/cli/src/commands/move.rs index 7d9eec1275..770b634109 100644 --- a/cli/src/commands/move.rs +++ b/cli/src/commands/move.rs @@ -18,8 +18,8 @@ use jj_lib::repo::Repo; use jj_lib::rewrite::merge_commit_trees; use tracing::instrument; -use super::combine_messages; use crate::cli_util::{user_error, CommandError, CommandHelper, RevisionArg}; +use crate::description_util::combine_messages; use crate::ui::Ui; /// Move changes from one revision into another diff --git a/cli/src/commands/split.rs b/cli/src/commands/split.rs index 000047f905..3eb747248a 100644 --- a/cli/src/commands/split.rs +++ b/cli/src/commands/split.rs @@ -22,8 +22,8 @@ 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::description_util::{diff_summary_to_description, edit_description}; use crate::diff_util::{self, DiffFormat}; use crate::formatter::PlainTextFormatter; use crate::ui::Ui; diff --git a/cli/src/commands/squash.rs b/cli/src/commands/squash.rs index ca9f2a6a37..8bd45d55b0 100644 --- a/cli/src/commands/squash.rs +++ b/cli/src/commands/squash.rs @@ -17,8 +17,8 @@ use jj_lib::backend::ObjectId; use jj_lib::revset; use tracing::instrument; -use super::combine_messages; use crate::cli_util::{self, user_error, CommandError, CommandHelper, RevisionArg}; +use crate::description_util::combine_messages; use crate::ui::Ui; /// Move changes from a revision into its parent diff --git a/cli/src/commands/unsquash.rs b/cli/src/commands/unsquash.rs index 2ffd44b573..2ccc911b4e 100644 --- a/cli/src/commands/unsquash.rs +++ b/cli/src/commands/unsquash.rs @@ -17,8 +17,8 @@ use jj_lib::matchers::EverythingMatcher; use jj_lib::rewrite::merge_commit_trees; use tracing::instrument; -use super::combine_messages; use crate::cli_util::{user_error, CommandError, CommandHelper, RevisionArg}; +use crate::description_util::combine_messages; use crate::ui::Ui; /// Move changes from a revision's parent into the revision diff --git a/cli/src/description_util.rs b/cli/src/description_util.rs new file mode 100644 index 0000000000..217ff225c9 --- /dev/null +++ b/cli/src/description_util.rs @@ -0,0 +1,117 @@ +use std::io::Write; +use std::{fs, io}; + +use itertools::Itertools; +use jj_lib::commit::Commit; +use jj_lib::matchers::EverythingMatcher; +use jj_lib::repo::ReadonlyRepo; +use jj_lib::settings::UserSettings; + +use crate::cli_util::{run_ui_editor, user_error, CommandError, WorkspaceCommandHelper}; +use crate::diff_util::{self, DiffFormat}; +use crate::formatter::PlainTextFormatter; +use crate::text_util; +use crate::ui::Ui; + +pub fn edit_description( + repo: &ReadonlyRepo, + description: &str, + settings: &UserSettings, +) -> Result { + let description_file_path = (|| -> Result<_, io::Error> { + let mut file = tempfile::Builder::new() + .prefix("editor-") + .suffix(".jjdescription") + .tempfile_in(repo.repo_path())?; + file.write_all(description.as_bytes())?; + file.write_all(b"\nJJ: Lines starting with \"JJ: \" (like this one) will be removed.\n")?; + let (_, path) = file.keep().map_err(|e| e.error)?; + Ok(path) + })() + .map_err(|e| { + user_error(format!( + r#"Failed to create description file in "{path}": {e}"#, + path = repo.repo_path().display() + )) + })?; + + run_ui_editor(settings, &description_file_path)?; + + let description = fs::read_to_string(&description_file_path).map_err(|e| { + user_error(format!( + r#"Failed to read description file "{path}": {e}"#, + path = description_file_path.display() + )) + })?; + // Delete the file only if everything went well. + // TODO: Tell the user the name of the file we left behind. + std::fs::remove_file(description_file_path).ok(); + // Normalize line ending, remove leading and trailing blank lines. + let description = description + .lines() + .filter(|line| !line.starts_with("JJ: ")) + .join("\n"); + Ok(text_util::complete_newline(description.trim_matches('\n'))) +} + +pub fn combine_messages( + repo: &ReadonlyRepo, + source: &Commit, + destination: &Commit, + settings: &UserSettings, + abandon_source: bool, +) -> Result { + let description = if abandon_source { + if source.description().is_empty() { + destination.description().to_string() + } else if destination.description().is_empty() { + source.description().to_string() + } else { + let combined = "JJ: Enter a description for the combined commit.\n".to_string() + + "JJ: Description from the destination commit:\n" + + destination.description() + + "\nJJ: Description from the source commit:\n" + + source.description(); + edit_description(repo, &combined, settings)? + } + } else { + destination.description().to_string() + }; + Ok(description) +} + +pub fn description_template_for_commit( + ui: &Ui, + settings: &UserSettings, + workspace_command: &WorkspaceCommandHelper, + commit: &Commit, +) -> Result { + let mut diff_summary_bytes = Vec::new(); + diff_util::show_patch( + ui, + &mut PlainTextFormatter::new(&mut diff_summary_bytes), + workspace_command, + commit, + &EverythingMatcher, + &[DiffFormat::Summary], + )?; + let description = if commit.description().is_empty() { + settings.default_description() + } else { + commit.description().to_owned() + }; + if diff_summary_bytes.is_empty() { + Ok(description) + } else { + Ok(description + "\n" + &diff_summary_to_description(&diff_summary_bytes)) + } +} + +pub 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.", + // Double-check this assumption for diffs that include file content. + ); + "JJ: This commit contains the following changes:\n".to_owned() + + &textwrap::indent(text, "JJ: ") +} diff --git a/cli/src/lib.rs b/cli/src/lib.rs index fc75e1cf0a..a274abef2c 100644 --- a/cli/src/lib.rs +++ b/cli/src/lib.rs @@ -19,6 +19,7 @@ pub mod cli_util; pub mod commands; pub mod commit_templater; pub mod config; +pub mod description_util; pub mod diff_util; pub mod formatter; pub mod graphlog;