From 97c6dd88f9e1536f2dda75cc26620969634331ef 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 | 20 +++++++ cli/src/commands/describe.rs | 41 ++++++++++++--- cli/src/text_util.rs | 35 +++++++++++++ cli/tests/cli-reference@.md.snap | 6 +++ cli/tests/test_commit_command.rs | 33 ++++++++++-- cli/tests/test_describe_command.rs | 84 +++++++++++++++++++++++------- 7 files changed, 192 insertions(+), 30 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 50ef5f51c4d..26020f82f84 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -60,6 +60,9 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). * `jj squash`: the `-k` flag can be used as a shorthand for `--keep-emptied`. +* `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..35beb4b796f 100644 --- a/cli/src/commands/commit.rs +++ b/cli/src/commands/commit.rs @@ -12,6 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. +use jj_lib::backend::Signature; use jj_lib::object_id::ObjectId; use jj_lib::repo::Repo; use tracing::instrument; @@ -22,6 +23,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::text_util::parse_author; use crate::ui::Ui; /// Update the description and create a new change on top. @@ -50,6 +52,16 @@ 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 author + /// timestamp for non-discardable commits. + #[arg( + long, + conflicts_with = "reset_author", + value_parser = parse_author + )] + author: Option<(String, String)>, } #[instrument(skip_all)] @@ -106,6 +118,14 @@ new working-copy commit. if args.reset_author { commit_builder.set_author(commit_builder.committer().clone()); } + if let Some((name, email)) = args.author.clone() { + let new_author = 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..3e4b703b844 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; @@ -30,6 +31,7 @@ use crate::description_util::edit_description; use crate::description_util::edit_multiple_descriptions; use crate::description_util::join_message_paragraphs; use crate::description_util::ParsedBulkEditMessage; +use crate::text_util::parse_author; use crate::ui::Ui; /// Update the change description or other metadata @@ -72,6 +74,16 @@ 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 author + /// timestamp for non-discardable commits. + #[arg( + long, + conflicts_with = "reset_author", + value_parser = parse_author + )] + author: Option<(String, String)>, } #[instrument(skip_all)] @@ -139,6 +151,14 @@ pub(crate) fn cmd_describe( let new_author = commit_builder.committer().clone(); commit_builder.set_author(new_author); } + if let Some((name, email)) = args.author.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)) }) @@ -194,13 +214,14 @@ pub(crate) fn cmd_describe( // `transform_descendants` below unnecessarily. let commit_descriptions: HashMap<_, _> = commit_descriptions .into_iter() - .filter_map(|(commit, new_description)| { - if *new_description == *commit.description() && !args.reset_author { - None - } else { - Some((commit.id(), new_description)) - } + .filter(|(commit, new_description)| { + new_description != commit.description() + || args.reset_author + || args.author.as_ref().is_some_and(|(name, email)| { + name != &commit.author().name || email != &commit.author().email + }) }) + .map(|(commit, new_description)| (commit.id(), new_description)) .collect(); let mut num_described = 0; @@ -225,6 +246,14 @@ pub(crate) fn cmd_describe( let new_author = commit_builder.committer().clone(); commit_builder = commit_builder.set_author(new_author); } + if let Some((name, email)) = args.author.clone() { + let new_author = Signature { + name, + email, + timestamp: commit_builder.author().timestamp.clone(), + }; + commit_builder = commit_builder.set_author(new_author); + } num_described += 1; } else { num_rebased += 1; diff --git a/cli/src/text_util.rs b/cli/src/text_util.rs index 26ab535826d..32c92dbc9ba 100644 --- a/cli/src/text_util.rs +++ b/cli/src/text_util.rs @@ -261,6 +261,12 @@ pub fn write_wrapped( }) } +pub fn parse_author(author: &str) -> Result<(String, String), &'static str> { + let re = regex::Regex::new(r"(?.*?)\s*<(?.+)>$").unwrap(); + let captures = re.captures(author).ok_or("Invalid author string")?; + Ok((captures["name"].to_string(), captures["email"].to_string())) +} + #[cfg(test)] mod tests { use std::io::Write as _; @@ -632,4 +638,33 @@ mod tests { "foo\n", ); } + + #[test] + fn test_parse_author() { + let expected_name = "Example"; + let expected_email = "example@example.com"; + let parsed = parse_author(&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_author(&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_author(&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 e8c52c65257..3d35d50088f 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 author timestamp for non-discardable commits. @@ -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 author timestamp for non-discardable commits. diff --git a/cli/tests/test_commit_command.rs b/cli/tests/test_commit_command.rs index 4d2a2c95be5..941526b2944 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", + "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:08) 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..e6510485c19 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", + "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:12.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", + "Super Seeder ", + ], + ); + insta::assert_snapshot!(get_signatures(), @r#" + @ Super Seeder super.seeder@example.com 2001-02-03 04:05:12.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: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 + │ 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:14.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: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 + │ 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:14.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]