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 Feb 16, 2025
1 parent 3ad956f commit 8f25e9c
Show file tree
Hide file tree
Showing 8 changed files with 259 additions and 1 deletion.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
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
148 changes: 148 additions & 0 deletions cli/src/commands/unsign.rs
Original file line number Diff line number Diff line change
@@ -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<RevisionArg>,
}

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()?;

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(())
}
14 changes: 14 additions & 0 deletions cli/tests/[email protected]
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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 <REVSETS>` — What revision(s) to unsign



## `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 @@ -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;
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()`.
");
}
82 changes: 82 additions & 0 deletions cli/tests/test_unsign_command.rs
Original file line number Diff line number Diff line change
@@ -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 [email protected] 2001-02-03 08:05:11 1d35ab70
│ (empty) (no description set)
○ kkmpptxz [email protected] 2001-02-03 08:05:11 0413d103 [✓︎]
│ (empty) three
○ rlvkpnrz [email protected] 2001-02-03 08:05:11 c8768375 [✓︎]
│ (empty) two
○ qpvuntsm [email protected] 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 [email protected] 2001-02-03 08:05:13 be9daa4d
│ (empty) (no description set)
○ kkmpptxz [email protected] 2001-02-03 08:05:13 7c11ee12
│ (empty) three
○ rlvkpnrz [email protected] 2001-02-03 08:05:13 deb0db4b
│ (empty) two
○ qpvuntsm [email protected] 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")
}
3 changes: 2 additions & 1 deletion docs/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down

0 comments on commit 8f25e9c

Please sign in to comment.