diff --git a/CHANGELOG.md b/CHANGELOG.md index 1ef19523750..46f4febdf06 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -56,6 +56,9 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). allows to set a name for the remote instead of using the default `origin`. +* `jj commit` and `jj describe` now accept `--author` option allowing to quickly change + author of given commit. + ### Fixed bugs * Fixed panic when parsing invalid conflict markers of a particular form. diff --git a/cli/src/commands/commit.rs b/cli/src/commands/commit.rs index 978a0c4ae41..f653b2549e7 100644 --- a/cli/src/commands/commit.rs +++ b/cli/src/commands/commit.rs @@ -22,6 +22,7 @@ use crate::command_error::CommandError; use crate::description_util::description_template; use crate::description_util::edit_description; use crate::description_util::join_message_paragraphs; +use crate::description_util::parse_user_name_email; use crate::ui::Ui; /// Update the description and create a new change on top. @@ -50,6 +51,11 @@ pub(crate) struct CommitArgs { /// $ JJ_USER='Foo Bar' JJ_EMAIL=foo@bar.com jj commit --reset-author #[arg(long)] reset_author: bool, + /// Set author to the provided string + /// + /// This changes author name and email while retaining timestamp. + #[arg(long, conflicts_with = "reset_author")] + author: Option, } #[instrument(skip_all)] @@ -106,6 +112,15 @@ new working-copy commit. if args.reset_author { commit_builder.set_author(commit_builder.committer().clone()); } + if let Some(author) = &args.author { + let (name, email) = parse_user_name_email(author)?; + let new_author = jj_lib::backend::Signature { + name, + email, + timestamp: commit_builder.author().timestamp.clone(), + }; + commit_builder.set_author(new_author); + } let description = if !args.message_paragraphs.is_empty() { join_message_paragraphs(&args.message_paragraphs) diff --git a/cli/src/commands/describe.rs b/cli/src/commands/describe.rs index b4c1641c8bd..865a249e735 100644 --- a/cli/src/commands/describe.rs +++ b/cli/src/commands/describe.rs @@ -29,6 +29,7 @@ use crate::description_util::description_template; use crate::description_util::edit_description; use crate::description_util::edit_multiple_descriptions; use crate::description_util::join_message_paragraphs; +use crate::description_util::parse_user_name_email; use crate::description_util::ParsedBulkEditMessage; use crate::ui::Ui; @@ -72,6 +73,11 @@ pub(crate) struct DescribeArgs { /// $ JJ_USER='Foo Bar' JJ_EMAIL=foo@bar.com jj describe --reset-author #[arg(long)] reset_author: bool, + /// Set author to the provided string + /// + /// This changes author name and email while retaining timestamp. + #[arg(long, conflicts_with = "reset_author")] + author: Option, } #[instrument(skip_all)] @@ -139,6 +145,15 @@ pub(crate) fn cmd_describe( let new_author = commit_builder.committer().clone(); commit_builder.set_author(new_author); } + if let Some(author) = &args.author { + let (name, email) = parse_user_name_email(author)?; + let new_author = jj_lib::backend::Signature { + name, + email, + timestamp: commit_builder.author().timestamp.clone(), + }; + commit_builder.set_author(new_author); + } let temp_commit = commit_builder.write_hidden()?; Ok((commit.id(), temp_commit)) }) diff --git a/cli/src/description_util.rs b/cli/src/description_util.rs index 7b13e3cc1b0..4ab9623f64b 100644 --- a/cli/src/description_util.rs +++ b/cli/src/description_util.rs @@ -14,6 +14,7 @@ use crate::cli_util::edit_temp_file; use crate::cli_util::short_commit_hash; use crate::cli_util::WorkspaceCommandHelper; use crate::cli_util::WorkspaceCommandTransaction; +use crate::command_error::cli_error; use crate::command_error::CommandError; use crate::formatter::PlainTextFormatter; use crate::text_util; @@ -251,6 +252,14 @@ pub fn description_template( Ok(output.into_string_lossy()) } +pub fn parse_user_name_email(author: &str) -> Result<(String, String), CommandError> { + let re = regex::Regex::new(r"(?.*?)\s*<(?.+)>").unwrap(); + let captures = re + .captures(author) + .ok_or_else(|| cli_error("Invalid author string"))?; + Ok((captures["name"].to_string(), captures["email"].to_string())) +} + #[cfg(test)] mod tests { use indexmap::indexmap; @@ -258,6 +267,7 @@ mod tests { use maplit::hashmap; use super::parse_bulk_edit_message; + use super::parse_user_name_email; use crate::description_util::ParseBulkEditMessageError; #[test] @@ -410,4 +420,33 @@ mod tests { assert!(result.duplicates.is_empty()); assert!(result.unexpected.is_empty()); } + + #[test] + fn test_parse_author() { + let expected_name = "Example"; + let expected_email = "example@example.com"; + let parsed = parse_user_name_email(&format!("{expected_name} <{expected_email}>")).unwrap(); + assert_eq!( + (expected_name.to_string(), expected_email.to_string()), + parsed + ); + } + + #[test] + fn test_parse_author_with_utf8() { + let expected_name = "Ąćęłńóśżź"; + let expected_email = "example@example.com"; + let parsed = parse_user_name_email(&format!("{expected_name} <{expected_email}>")).unwrap(); + assert_eq!( + (expected_name.to_string(), expected_email.to_string()), + parsed + ); + } + + #[test] + fn test_parse_author_without_name() { + let expected_email = "example@example.com"; + let parsed = parse_user_name_email(&format!("<{expected_email}>")).unwrap(); + assert_eq!(("".to_string(), expected_email.to_string()), parsed); + } } diff --git a/cli/tests/cli-reference@.md.snap b/cli/tests/cli-reference@.md.snap index 8d255d7af6a..79f4fd8c7f7 100644 --- a/cli/tests/cli-reference@.md.snap +++ b/cli/tests/cli-reference@.md.snap @@ -446,6 +446,9 @@ Update the description and create a new change on top You can use it in combination with the JJ_USER and JJ_EMAIL environment variables to set a different author: $ JJ_USER='Foo Bar' JJ_EMAIL=foo@bar.com jj commit --reset-author +* `--author ` — Set author to the provided string + + This changes author name and email while retaining timestamp. @@ -599,6 +602,9 @@ Starts an editor to let you edit the description of changes. The editor will be You can use it in combination with the JJ_USER and JJ_EMAIL environment variables to set a different author: $ JJ_USER='Foo Bar' JJ_EMAIL=foo@bar.com jj describe --reset-author +* `--author ` — Set author to the provided string + + This changes author name and email while retaining timestamp.