From a4316ea3f536ac98c7e77e9cb30490e05dae7678 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Miku=C5=82a?= Date: Sat, 14 Sep 2024 16:51:52 +0200 Subject: [PATCH] cli: add `--author` argument for `commit` and `describe` --- CHANGELOG.md | 3 ++ cli/src/commands/commit.rs | 15 ++++++ cli/src/commands/describe.rs | 34 +++++++++++- cli/src/description_util.rs | 39 ++++++++++++++ cli/tests/cli-reference@.md.snap | 6 +++ cli/tests/test_commit_command.rs | 33 ++++++++++-- cli/tests/test_describe_command.rs | 84 +++++++++++++++++++++++------- 7 files changed, 189 insertions(+), 25 deletions(-) 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..19937c60326 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. + #[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.committer().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..7413a9245d6 100644 --- a/cli/src/commands/describe.rs +++ b/cli/src/commands/describe.rs @@ -17,6 +17,7 @@ use std::io; use std::io::Read; use itertools::Itertools; +use jj_lib::backend::Signature; use jj_lib::commit::CommitIteratorExt; use jj_lib::object_id::ObjectId; use tracing::instrument; @@ -29,6 +30,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 +74,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)] @@ -112,6 +119,12 @@ pub(crate) fn cmd_describe( None }; + let author_arg = args + .author + .as_deref() + .map(parse_user_name_email) + .transpose()?; + let commit_descriptions: Vec<(_, _)> = if args.no_edit || shared_description.is_some() { commits .iter() @@ -139,6 +152,14 @@ pub(crate) fn cmd_describe( let new_author = commit_builder.committer().clone(); commit_builder.set_author(new_author); } + if let Some((name, email)) = author_arg.clone() { + let new_author = Signature { + name, + email, + timestamp: commit.author().timestamp.clone(), + }; + commit_builder.set_author(new_author); + } let temp_commit = commit_builder.write_hidden()?; Ok((commit.id(), temp_commit)) }) @@ -195,7 +216,10 @@ pub(crate) fn cmd_describe( let commit_descriptions: HashMap<_, _> = commit_descriptions .into_iter() .filter_map(|(commit, new_description)| { - if *new_description == *commit.description() && !args.reset_author { + if *new_description == *commit.description() + && !args.reset_author + && author_arg.is_none() + { None } else { Some((commit.id(), new_description)) @@ -218,6 +242,11 @@ pub(crate) fn cmd_describe( .collect_vec(), |rewriter| { let old_commit_id = rewriter.old_commit().id().clone(); + let new_author = author_arg.clone().map(|(name, email)| Signature { + name, + email, + timestamp: rewriter.old_commit().author().timestamp.clone(), + }); let mut commit_builder = rewriter.rebase(command.settings())?; if let Some(description) = commit_descriptions.get(&old_commit_id) { commit_builder = commit_builder.set_description(description); @@ -225,6 +254,9 @@ pub(crate) fn cmd_describe( let new_author = commit_builder.committer().clone(); commit_builder = commit_builder.set_author(new_author); } + if let Some(new_author) = new_author { + commit_builder = commit_builder.set_author(new_author); + } num_described += 1; } else { num_rebased += 1; 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..8debb63d7f9 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. @@ -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. diff --git a/cli/tests/test_commit_command.rs b/cli/tests/test_commit_command.rs index 4d2a2c95be5..ffa3d49731a 100644 --- a/cli/tests/test_commit_command.rs +++ b/cli/tests/test_commit_command.rs @@ -215,6 +215,7 @@ fn test_commit_with_description_template() { std::fs::write(workspace_path.join("file1"), "foo\n").unwrap(); std::fs::write(workspace_path.join("file2"), "bar\n").unwrap(); + std::fs::write(workspace_path.join("file3"), "foobar\n").unwrap(); // Only file1 should be included in the diff test_env.jj_cmd_ok(&workspace_path, &["commit", "file1"]); @@ -230,19 +231,41 @@ fn test_commit_with_description_template() { JJ: Lines starting with "JJ: " (like this one) will be removed. "###); - // Timestamp after the reset should be available to the template - test_env.jj_cmd_ok(&workspace_path, &["commit", "--reset-author"]); + // Only file2 with modified author should be included in the diff + test_env.jj_cmd_ok( + &workspace_path, + &[ + "commit", + "--author", + r#""Another User ""#, + "file2", + ], + ); insta::assert_snapshot!( - std::fs::read_to_string(test_env.env_root().join("editor")).unwrap(), @r###" + std::fs::read_to_string(test_env.env_root().join("editor")).unwrap(), @r#" - JJ: Author: Test User (2001-02-03 08:05:09) + JJ: Author: "Another User (2001-02-03 08:05:09) JJ: Committer: Test User (2001-02-03 08:05:09) JJ: file2 | 1 + JJ: 1 file changed, 1 insertion(+), 0 deletions(-) JJ: Lines starting with "JJ: " (like this one) will be removed. - "###); + "#); + + // Timestamp after the reset should be available to the template + test_env.jj_cmd_ok(&workspace_path, &["commit", "--reset-author"]); + insta::assert_snapshot!( + std::fs::read_to_string(test_env.env_root().join("editor")).unwrap(), @r#" + + JJ: Author: Test User (2001-02-03 08:05:10) + JJ: Committer: Test User (2001-02-03 08:05:10) + + JJ: file3 | 1 + + JJ: 1 file changed, 1 insertion(+), 0 deletions(-) + + JJ: Lines starting with "JJ: " (like this one) will be removed. + "#); } #[test] diff --git a/cli/tests/test_describe_command.rs b/cli/tests/test_describe_command.rs index 29898c6bba7..c20dd2c426d 100644 --- a/cli/tests/test_describe_command.rs +++ b/cli/tests/test_describe_command.rs @@ -525,21 +525,19 @@ fn test_describe_author() { ~ "###); - // Reset the author for the latest commit (the committer is always reset) + // Change the author for the latest commit (the committer is always reset) test_env.jj_cmd_ok( &repo_path, &[ "describe", - "--config-toml", - r#"user.name = "Ove Ridder" - user.email = "ove.ridder@example.com""#, "--no-edit", - "--reset-author", + "--author", + r#""Super Seeder ""#, ], ); - insta::assert_snapshot!(get_signatures(), @r###" - @ Ove Ridder ove.ridder@example.com 2001-02-03 04:05:12.000 +07:00 - │ Ove Ridder ove.ridder@example.com 2001-02-03 04:05:12.000 +07:00 + insta::assert_snapshot!(get_signatures(), @r#" + @ "Super Seeder super.seeder@example.com 2001-02-03 04:05:10.000 +07:00 + │ Test User test.user@example.com 2001-02-03 04:05:12.000 +07:00 ○ Test User test.user@example.com 2001-02-03 04:05:09.000 +07:00 │ Test User test.user@example.com 2001-02-03 04:05:09.000 +07:00 ○ Test User test.user@example.com 2001-02-03 04:05:08.000 +07:00 @@ -547,7 +545,55 @@ fn test_describe_author() { ○ Test User test.user@example.com 2001-02-03 04:05:07.000 +07:00 │ Test User test.user@example.com 2001-02-03 04:05:07.000 +07:00 ~ - "###); + "#); + + // Change the author for multiple commits (the committer is always reset) + test_env.jj_cmd_ok( + &repo_path, + &[ + "describe", + "@---", + "@-", + "--no-edit", + "--author", + r#""Super Seeder ""#, + ], + ); + insta::assert_snapshot!(get_signatures(), @r#" + @ "Super Seeder super.seeder@example.com 2001-02-03 04:05:10.000 +07:00 + │ Test User test.user@example.com 2001-02-03 04:05:14.000 +07:00 + ○ "Super Seeder super.seeder@example.com 2001-02-03 04:05:09.000 +07:00 + │ Test User test.user@example.com 2001-02-03 04:05:14.000 +07:00 + ○ Test User test.user@example.com 2001-02-03 04:05:14.000 +07:00 + │ Test User test.user@example.com 2001-02-03 04:05:14.000 +07:00 + ○ "Super Seeder super.seeder@example.com 2001-02-03 04:05:07.000 +07:00 + │ Test User test.user@example.com 2001-02-03 04:05:14.000 +07:00 + ~ + "#); + + // Reset the author for the latest commit (the committer is always reset) + test_env.jj_cmd_ok( + &repo_path, + &[ + "describe", + "--config-toml", + r#"user.name = "Ove Ridder" + user.email = "ove.ridder@example.com""#, + "--no-edit", + "--reset-author", + ], + ); + insta::assert_snapshot!(get_signatures(), @r#" + @ Ove Ridder ove.ridder@example.com 2001-02-03 04:05:16.000 +07:00 + │ Ove Ridder ove.ridder@example.com 2001-02-03 04:05:16.000 +07:00 + ○ "Super Seeder super.seeder@example.com 2001-02-03 04:05:09.000 +07:00 + │ Test User test.user@example.com 2001-02-03 04:05:14.000 +07:00 + ○ Test User test.user@example.com 2001-02-03 04:05:14.000 +07:00 + │ Test User test.user@example.com 2001-02-03 04:05:14.000 +07:00 + ○ "Super Seeder super.seeder@example.com 2001-02-03 04:05:07.000 +07:00 + │ Test User test.user@example.com 2001-02-03 04:05:14.000 +07:00 + ~ + "#); // Reset the author for multiple commits (the committer is always reset) test_env.jj_cmd_ok( @@ -563,17 +609,17 @@ fn test_describe_author() { "--reset-author", ], ); - insta::assert_snapshot!(get_signatures(), @r###" - @ Ove Ridder ove.ridder@example.com 2001-02-03 04:05:14.000 +07:00 - │ Ove Ridder ove.ridder@example.com 2001-02-03 04:05:14.000 +07:00 - ○ Ove Ridder ove.ridder@example.com 2001-02-03 04:05:14.000 +07:00 - │ Ove Ridder ove.ridder@example.com 2001-02-03 04:05:14.000 +07:00 - ○ Test User test.user@example.com 2001-02-03 04:05:08.000 +07:00 - │ Ove Ridder ove.ridder@example.com 2001-02-03 04:05:14.000 +07:00 - ○ Ove Ridder ove.ridder@example.com 2001-02-03 04:05:14.000 +07:00 - │ Ove Ridder ove.ridder@example.com 2001-02-03 04:05:14.000 +07:00 + insta::assert_snapshot!(get_signatures(), @r#" + @ Ove Ridder ove.ridder@example.com 2001-02-03 04:05:18.000 +07:00 + │ Ove Ridder ove.ridder@example.com 2001-02-03 04:05:18.000 +07:00 + ○ Ove Ridder ove.ridder@example.com 2001-02-03 04:05:18.000 +07:00 + │ Ove Ridder ove.ridder@example.com 2001-02-03 04:05:18.000 +07:00 + ○ Test User test.user@example.com 2001-02-03 04:05:14.000 +07:00 + │ Ove Ridder ove.ridder@example.com 2001-02-03 04:05:18.000 +07:00 + ○ Ove Ridder ove.ridder@example.com 2001-02-03 04:05:18.000 +07:00 + │ Ove Ridder ove.ridder@example.com 2001-02-03 04:05:18.000 +07:00 ~ - "###); + "#); } #[test]