Skip to content

Commit

Permalink
unsign: implement jj unsign command
Browse files Browse the repository at this point in the history
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 <[email protected]>
Co-authored-by: necauqua <[email protected]>
  • Loading branch information
3 people committed Jan 22, 2025
1 parent 98275da commit 6842c60
Show file tree
Hide file tree
Showing 8 changed files with 298 additions and 1 deletion.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,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

* Fixed diff selection by external tools with `jj split`/`commit -i FILESETS`.
Expand Down
3 changes: 3 additions & 0 deletions cli/src/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ mod split;
mod squash;
mod status;
mod tag;
mod unsign;
mod unsquash;
mod util;
mod version;
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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);
Expand Down
116 changes: 116 additions & 0 deletions cli/src/commands/unsign.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
// 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 super::sign::check_commits_authored_by;
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,
default_value = "@",
value_name = "REVSETS",
add = ArgValueCandidates::new(complete::mutable_revisions),
)]
revisions: Vec<RevisionArg>,
/// Unsign a commit that is not authored by you.
#[arg(long, short)]
allow_not_mine: bool,
}

pub fn cmd_unsign(
ui: &mut Ui,
command: &CommandHelper,
args: &UnsignArgs,
) -> Result<(), CommandError> {
let mut workspace_command = command.workspace_helper(ui)?;

let commits: IndexSet<Commit> = workspace_command
.parse_union_revsets(ui, &args.revisions)?
.evaluate_to_commits()?
.try_collect()?;

if !args.allow_not_mine {
check_commits_authored_by(
&commits,
workspace_command.settings().user_email(),
"Use --allow-not-mine to unsign anyway.",
)?;
}

workspace_command.check_rewritable(commits.iter().ids())?;

let mut tx = workspace_command.start_transaction();

let mut unsigned_commits = vec![];
tx.repo_mut().transform_descendants(
commits.iter().ids().cloned().collect_vec(),
|rewriter| {
if commits.contains(rewriter.old_commit()) {
let commit_builder = rewriter.reparent();
let new_commit = commit_builder
.set_sign_behavior(SignBehavior::Drop)
.write()?;
unsigned_commits.push(new_commit);
}
Ok(())
},
)?;

if let Some(mut formatter) = ui.status_formatter() {
match unsigned_commits.len() {
0 => (),
1 => {
write!(formatter, "Unsigned commit ")?;
tx.base_workspace_helper()
.write_commit_summary(formatter.as_mut(), &unsigned_commits[0])?;
writeln!(ui.status())?;
}
2.. => {
let template = tx.base_workspace_helper().commit_summary_template();
writeln!(formatter, "Unsigned the following commits:")?;
for commit in &unsigned_commits {
write!(formatter, " ")?;
template.format(commit, formatter.as_mut())?;
writeln!(formatter)?;
}
}
};
}
let transaction_description = match unsigned_commits.len() {
0 => "".to_string(),
1 => format!("unsign commit {}", unsigned_commits[0].id()),
2.. => format!(
"unsign commit {} and {} more",
unsigned_commits[0].id(),
unsigned_commits.len() - 1
),
};
tx.finish(ui, transaction_description)?;

Ok(())
}
17 changes: 17 additions & 0 deletions cli/tests/[email protected]
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,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)
Expand Down Expand Up @@ -160,6 +161,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

Expand Down Expand Up @@ -2416,6 +2418,21 @@ Undo an operation (shortcut for `jj op undo`)



## `jj unsign`

Drop a cryptographic signature

**Usage:** `jj unsign [OPTIONS]`

###### **Options:**

* `-r`, `--revisions <REVSETS>` — What revision(s) to unsign

Default value: `@`
* `-a`, `--allow-not-mine` — Unsign a commit that is not authored by you



## `jj version`

Display version information
Expand Down
1 change: 1 addition & 0 deletions cli/tests/runner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,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;
Expand Down
7 changes: 7 additions & 0 deletions cli/tests/test_immutable_commits.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()`.
");
}
150 changes: 150 additions & 0 deletions cli/tests/test_unsign_command.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
// 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#"
[signing]
sign-all = false
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"]);

let template = r#"if(signature,
signature.status() ++ " " ++ signature.display(),
"no"
) ++ " signature""#;

let show_no_sig = test_env.jj_cmd_success(&repo_path, &["log", "-T", template, "-r", "all()"]);
insta::assert_snapshot!(show_no_sig, @r"
@ no signature
○ no signature
○ no signature
○ no signature
◆ no signature
");

test_env.jj_cmd_ok(&repo_path, &["sign", "-r", "..@-"]);

let show_with_sig =
test_env.jj_cmd_success(&repo_path, &["log", "-T", template, "-r", "all()"]);
insta::assert_snapshot!(show_with_sig, @r"
@ no signature
○ good test-display signature
○ good test-display signature
○ good test-display signature
◆ no signature
");

let (_, stderr) = test_env.jj_cmd_ok(&repo_path, &["unsign", "-r", "..@-"]);
insta::assert_snapshot!(stderr, @r"
Unsigned the following commits:
qpvuntsm hidden afde6e4b (empty) one
rlvkpnrz hidden d49204af (empty) two
kkmpptxz hidden ea6d9b6d (empty) three
Rebased 1 descendant commits
Working copy now at: zsuskuln 4029f2fc (empty) (no description set)
Parent commit : kkmpptxz ea6d9b6d (empty) three
");

let show_with_sig =
test_env.jj_cmd_success(&repo_path, &["log", "-T", template, "-r", "all()"]);
insta::assert_snapshot!(show_with_sig, @r"
@ no signature
○ no signature
○ no signature
○ no signature
◆ no signature
");
}

#[test]
fn test_allow_not_mine() {
let test_env = TestEnvironment::default();

test_env.add_config(
r#"
[signing]
sign-all = false
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", "init"]);
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"]);

let (_, stderr) = test_env.jj_cmd_ok(&repo_path, &["sign", "-r", "..@-"]);
insta::assert_snapshot!(stderr, @r"
Signed the following commits:
qpvuntsm hidden 59bce38c (empty) init
rlvkpnrz hidden 2ab1ce70 (empty) one
kkmpptxz hidden 00f3af95 (empty) two
zsuskuln hidden 648894e4 (empty) three
Rebased 1 descendant commits
Working copy now at: mzvwutvl 7349dba8 (empty) (no description set)
Parent commit : zsuskuln 648894e4 (empty) three
");
test_env.jj_cmd_ok(
&repo_path,
&[
"desc",
"--author",
"Someone Else <[email protected]>",
"--no-edit",
"..@-",
],
);
let stderr = test_env.jj_cmd_failure(&repo_path, &["unsign", "-r", "@-"]);
insta::assert_snapshot!(stderr, @r"
Error: Commit 88d29a30391f is not authored by you.
Hint: Use --allow-not-mine to unsign anyway.
");

let stderr = test_env.jj_cmd_failure(&repo_path, &["unsign", "-r", "..@-"]);
insta::assert_snapshot!(stderr, @r"
Error: The following commits are not authored by you:
88d29a30391f
78855514c988
8e11694d102e
0180dfa7875d
Hint: Use --allow-not-mine to unsign anyway.
");

let (_, stderr) = test_env.jj_cmd_ok(&repo_path, &["unsign", "-r", "..@-", "--allow-not-mine"]);
insta::assert_snapshot!(stderr, @r"
Unsigned the following commits:
qpvuntsm hidden a6a855c8 (empty) init
rlvkpnrz hidden 99410553 (empty) one
kkmpptxz hidden bee2d845 (empty) two
zsuskuln hidden c0b9a9d9 (empty) three
Rebased 1 descendant commits
Working copy now at: mzvwutvl c7afb8c1 (empty) (no description set)
Parent commit : zsuskuln c0b9a9d9 (empty) three
");
}
3 changes: 2 additions & 1 deletion docs/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -1097,7 +1097,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

Expand Down

0 comments on commit 6842c60

Please sign in to comment.