From 8f25e9c5af231f9f017052451035ad8c1e474257 Mon Sep 17 00:00:00 2001 From: pylbrecht Date: Sat, 11 Jan 2025 21:53:47 +0100 Subject: [PATCH] unsign: implement `jj unsign` command Commits to be unsigned, which are signed and not authored by the user, require `--allow-not-mine`. The output of `jj unsign` is based on that of `jj abandon`. --- Co-authored-by: julienvincent Co-authored-by: necauqua --- CHANGELOG.md | 2 + cli/src/commands/mod.rs | 3 + cli/src/commands/unsign.rs | 148 ++++++++++++++++++++++++++++ cli/tests/cli-reference@.md.snap | 14 +++ cli/tests/runner.rs | 1 + cli/tests/test_immutable_commits.rs | 7 ++ cli/tests/test_unsign_command.rs | 82 +++++++++++++++ docs/config.md | 3 +- 8 files changed, 259 insertions(+), 1 deletion(-) create mode 100644 cli/src/commands/unsign.rs create mode 100644 cli/tests/test_unsign_command.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 862ccb995b..22f995a9db 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -240,6 +240,8 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). * The new `jj sign` command allows signing commits. +* The new `jj unsign` command allows unsigning commits. + ### Fixed bugs * `jj git fetch` with multiple remotes will now fetch from all remotes before diff --git a/cli/src/commands/mod.rs b/cli/src/commands/mod.rs index ad7703a6c9..19e70bf373 100644 --- a/cli/src/commands/mod.rs +++ b/cli/src/commands/mod.rs @@ -53,6 +53,7 @@ mod split; mod squash; mod status; mod tag; +mod unsign; mod unsquash; mod util; mod version; @@ -144,6 +145,7 @@ enum Command { Util(util::UtilCommand), /// Undo an operation (shortcut for `jj op undo`) Undo(operation::undo::OperationUndoArgs), + Unsign(unsign::UnsignArgs), // TODO: Delete `unsquash` in jj 0.28+ #[command(hide = true)] Unsquash(unsquash::UnsquashArgs), @@ -219,6 +221,7 @@ pub fn run_command(ui: &mut Ui, command_helper: &CommandHelper) -> Result<(), Co Command::Status(args) => status::cmd_status(ui, command_helper, args), Command::Tag(args) => tag::cmd_tag(ui, command_helper, args), Command::Undo(args) => operation::undo::cmd_op_undo(ui, command_helper, args), + Command::Unsign(args) => unsign::cmd_unsign(ui, command_helper, args), Command::Unsquash(args) => unsquash::cmd_unsquash(ui, command_helper, args), Command::Untrack(args) => { let cmd = renamed_cmd("untrack", "file untrack", file::untrack::cmd_file_untrack); diff --git a/cli/src/commands/unsign.rs b/cli/src/commands/unsign.rs new file mode 100644 index 0000000000..e363e86f02 --- /dev/null +++ b/cli/src/commands/unsign.rs @@ -0,0 +1,148 @@ +// Copyright 2023 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 clap_complete::ArgValueCandidates; +use indexmap::IndexSet; +use itertools::Itertools; +use jj_lib::commit::Commit; +use jj_lib::commit::CommitIteratorExt; +use jj_lib::signing::SignBehavior; + +use crate::cli_util::CommandHelper; +use crate::cli_util::RevisionArg; +use crate::command_error::CommandError; +use crate::complete; +use crate::ui::Ui; + +/// Drop a cryptographic signature +#[derive(clap::Args, Clone, Debug)] +pub struct UnsignArgs { + /// What revision(s) to unsign + #[arg( + long, short, + value_name = "REVSETS", + add = ArgValueCandidates::new(complete::mutable_revisions), + )] + revisions: Vec, +} + +pub fn cmd_unsign( + ui: &mut Ui, + command: &CommandHelper, + args: &UnsignArgs, +) -> Result<(), CommandError> { + let mut workspace_command = command.workspace_helper(ui)?; + + let commits: IndexSet = workspace_command + .parse_union_revsets(ui, &args.revisions)? + .evaluate_to_commits()? + .try_collect()?; + + workspace_command.check_rewritable(commits.iter().ids())?; + + let mut tx = workspace_command.start_transaction(); + + let mut unsigned_commits = vec![]; + let mut foreign_commits = vec![]; + let mut num_reparented = 0; + let is_authored_by_me = + |commit: &Commit| -> bool { commit.author().email == command.settings().user_email() }; + + tx.repo_mut().transform_descendants( + commits.iter().ids().cloned().collect_vec(), + |rewriter| { + let old_commit = rewriter.old_commit().clone(); + let commit_builder = rewriter.reparent(); + + if commits.contains(&old_commit) { + let new_commit = commit_builder + .set_sign_behavior(SignBehavior::Drop) + .write()?; + unsigned_commits.push(new_commit.clone()); + + if old_commit.is_signed() && !is_authored_by_me(&old_commit) { + foreign_commits.push(new_commit); + } + } else { + commit_builder.write()?; + num_reparented += 1; + } + Ok(()) + }, + )?; + + if let Some(mut formatter) = ui.status_formatter() { + match &*unsigned_commits { + [] => {} + [commit] => { + write!(formatter, "Unsigned commit ")?; + tx.base_workspace_helper() + .write_commit_summary(formatter.as_mut(), commit)?; + writeln!(ui.status())?; + } + commits => { + let template = tx.base_workspace_helper().commit_summary_template(); + writeln!(formatter, "Unsigned the following commits:")?; + for commit in commits { + write!(formatter, " ")?; + template.format(commit, formatter.as_mut())?; + writeln!(formatter)?; + } + } + }; + + match &*foreign_commits { + [] => {} + [commit] => { + write!( + ui.warning_default(), + "Unsigned 1 commit not authored by you" + )?; + tx.base_workspace_helper() + .write_commit_summary(formatter.as_mut(), commit)?; + writeln!(ui.status())?; + } + commits => { + let template = tx.base_workspace_helper().commit_summary_template(); + writeln!( + ui.warning_default(), + "Unsigned {} commits not authored by you", + commits.len() + )?; + for commit in commits { + write!(formatter, " ")?; + template.format(commit, formatter.as_mut())?; + writeln!(formatter)?; + } + } + }; + } + + if num_reparented > 0 { + writeln!(ui.status(), "Rebased {num_reparented} descendant commits")?; + } + + let transaction_description = match &*unsigned_commits { + [] => "".to_string(), + [commit] => format!("unsign commit {}", commit.id()), + commits => format!( + "unsign commit {} and {} more", + commits[0].id(), + commits.len() - 1 + ), + }; + tx.finish(ui, transaction_description)?; + + Ok(()) +} diff --git a/cli/tests/cli-reference@.md.snap b/cli/tests/cli-reference@.md.snap index 6b57b13f2a..361e2130bc 100644 --- a/cli/tests/cli-reference@.md.snap +++ b/cli/tests/cli-reference@.md.snap @@ -99,6 +99,7 @@ This document contains the help content for the `jj` command-line program. * [`jj util install-man-pages`↴](#jj-util-install-man-pages) * [`jj util markdown-help`↴](#jj-util-markdown-help) * [`jj undo`↴](#jj-undo) +* [`jj unsign`↴](#jj-unsign) * [`jj version`↴](#jj-version) * [`jj workspace`↴](#jj-workspace) * [`jj workspace add`↴](#jj-workspace-add) @@ -159,6 +160,7 @@ To get started, see the tutorial at https://jj-vcs.github.io/jj/latest/tutorial/ * `tag` — Manage tags * `util` — Infrequently used commands such as for generating shell completions * `undo` — Undo an operation (shortcut for `jj op undo`) +* `unsign` — Drop a cryptographic signature * `version` — Display version information * `workspace` — Commands for working with workspaces @@ -2597,6 +2599,18 @@ Undo an operation (shortcut for `jj op undo`) +## `jj unsign` + +Drop a cryptographic signature + +**Usage:** `jj unsign [OPTIONS]` + +###### **Options:** + +* `-r`, `--revisions ` — What revision(s) to unsign + + + ## `jj version` Display version information diff --git a/cli/tests/runner.rs b/cli/tests/runner.rs index 25be88b731..e8129b00bf 100644 --- a/cli/tests/runner.rs +++ b/cli/tests/runner.rs @@ -73,6 +73,7 @@ mod test_status_command; mod test_tag_command; mod test_templater; mod test_undo; +mod test_unsign_command; mod test_unsquash_command; mod test_util_command; mod test_working_copy; diff --git a/cli/tests/test_immutable_commits.rs b/cli/tests/test_immutable_commits.rs index 763f5f6517..7a820526c0 100644 --- a/cli/tests/test_immutable_commits.rs +++ b/cli/tests/test_immutable_commits.rs @@ -354,4 +354,11 @@ fn test_rewrite_immutable_commands() { Hint: Could not modify commit: mzvwutvl bcab555f main | (conflict) merge Hint: Pass `--ignore-immutable` or configure the set of immutable commits via `revset-aliases.immutable_heads()`. "); + // unsign + let stderr = test_env.jj_cmd_failure(&repo_path, &["unsign", "-r=main"]); + insta::assert_snapshot!(stderr, @r" + Error: Commit bcab555fc80e is immutable + Hint: Could not modify commit: mzvwutvl bcab555f main | (conflict) merge + Hint: Pass `--ignore-immutable` or configure the set of immutable commits via `revset-aliases.immutable_heads()`. + "); } diff --git a/cli/tests/test_unsign_command.rs b/cli/tests/test_unsign_command.rs new file mode 100644 index 0000000000..1b602b84bc --- /dev/null +++ b/cli/tests/test_unsign_command.rs @@ -0,0 +1,82 @@ +// Copyright 2023 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 crate::common::TestEnvironment; + +#[test] +fn test_unsign() { + let test_env = TestEnvironment::default(); + + test_env.add_config( + r#" +[ui] +show-cryptographic-signatures = true + +[signing] +behavior = "keep" +backend = "test" +"#, + ); + + test_env.jj_cmd_ok(test_env.env_root(), &["git", "init", "repo"]); + let repo_path = test_env.env_root().join("repo"); + test_env.jj_cmd_ok(&repo_path, &["commit", "-m", "one"]); + test_env.jj_cmd_ok(&repo_path, &["commit", "-m", "two"]); + test_env.jj_cmd_ok(&repo_path, &["commit", "-m", "three"]); + + test_env.jj_cmd_ok(&repo_path, &["sign", "-r", "..@-"]); + + let stdout = test_env.jj_cmd_success(&repo_path, &["log", "-r", "all()"]); + insta::assert_snapshot!(stdout, @r" + @ zsuskuln test.user@example.com 2001-02-03 08:05:11 1d35ab70 + │ (empty) (no description set) + ○ kkmpptxz test.user@example.com 2001-02-03 08:05:11 0413d103 [✓︎] + │ (empty) three + ○ rlvkpnrz test.user@example.com 2001-02-03 08:05:11 c8768375 [✓︎] + │ (empty) two + ○ qpvuntsm test.user@example.com 2001-02-03 08:05:11 b90f5370 [✓︎] + │ (empty) one + ◆ zzzzzzzz root() 00000000 + "); + + let (_, stderr) = test_env.jj_cmd_ok(&repo_path, &["unsign", "-r", "..@-"]); + insta::assert_snapshot!(stderr, @r" + Unsigned the following commits: + qpvuntsm hidden cb05440c (empty) one + rlvkpnrz hidden deb0db4b (empty) two + kkmpptxz hidden 7c11ee12 (empty) three + Rebased 1 descendant commits + Working copy now at: zsuskuln be9daa4d (empty) (no description set) + Parent commit : kkmpptxz 7c11ee12 (empty) three + "); + + let stdout = test_env.jj_cmd_success(&repo_path, &["log", "-r", "all()"]); + insta::assert_snapshot!(stdout, @r" + @ zsuskuln test.user@example.com 2001-02-03 08:05:13 be9daa4d + │ (empty) (no description set) + ○ kkmpptxz test.user@example.com 2001-02-03 08:05:13 7c11ee12 + │ (empty) three + ○ rlvkpnrz test.user@example.com 2001-02-03 08:05:13 deb0db4b + │ (empty) two + ○ qpvuntsm test.user@example.com 2001-02-03 08:05:13 cb05440c + │ (empty) one + ◆ zzzzzzzz root() 00000000 + "); +} + +#[test] +#[should_panic] +fn test_warn_about_unsigning_commits_not_authored_by_me() { + todo!("figure out how to sign commits with different identities in testenv") +} diff --git a/docs/config.md b/docs/config.md index 04cdc44b38..8d4f1d2e52 100644 --- a/docs/config.md +++ b/docs/config.md @@ -1196,7 +1196,8 @@ sign-on-push = true ### Manually signing commits -You can use [`jj sign`](./cli-reference.md#jj-sign) to manually sign commits. +You can use [`jj sign`](./cli-reference.md#jj-sign)/[`jj unsign`](./cli-reference.md#jj-unsign) +to sign/unsign commits manually. ## Commit Signature Verification