Skip to content

Commit 8f25e9c

Browse files
pylbrechtjulienvincentnecauqua
committed
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 <[email protected]> Co-authored-by: necauqua <[email protected]>
1 parent 3ad956f commit 8f25e9c

File tree

8 files changed

+259
-1
lines changed

8 files changed

+259
-1
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,8 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
240240

241241
* The new `jj sign` command allows signing commits.
242242

243+
* The new `jj unsign` command allows unsigning commits.
244+
243245
### Fixed bugs
244246

245247
* `jj git fetch` with multiple remotes will now fetch from all remotes before

cli/src/commands/mod.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ mod split;
5353
mod squash;
5454
mod status;
5555
mod tag;
56+
mod unsign;
5657
mod unsquash;
5758
mod util;
5859
mod version;
@@ -144,6 +145,7 @@ enum Command {
144145
Util(util::UtilCommand),
145146
/// Undo an operation (shortcut for `jj op undo`)
146147
Undo(operation::undo::OperationUndoArgs),
148+
Unsign(unsign::UnsignArgs),
147149
// TODO: Delete `unsquash` in jj 0.28+
148150
#[command(hide = true)]
149151
Unsquash(unsquash::UnsquashArgs),
@@ -219,6 +221,7 @@ pub fn run_command(ui: &mut Ui, command_helper: &CommandHelper) -> Result<(), Co
219221
Command::Status(args) => status::cmd_status(ui, command_helper, args),
220222
Command::Tag(args) => tag::cmd_tag(ui, command_helper, args),
221223
Command::Undo(args) => operation::undo::cmd_op_undo(ui, command_helper, args),
224+
Command::Unsign(args) => unsign::cmd_unsign(ui, command_helper, args),
222225
Command::Unsquash(args) => unsquash::cmd_unsquash(ui, command_helper, args),
223226
Command::Untrack(args) => {
224227
let cmd = renamed_cmd("untrack", "file untrack", file::untrack::cmd_file_untrack);

cli/src/commands/unsign.rs

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
// Copyright 2023 The Jujutsu Authors
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// https://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
use clap_complete::ArgValueCandidates;
16+
use indexmap::IndexSet;
17+
use itertools::Itertools;
18+
use jj_lib::commit::Commit;
19+
use jj_lib::commit::CommitIteratorExt;
20+
use jj_lib::signing::SignBehavior;
21+
22+
use crate::cli_util::CommandHelper;
23+
use crate::cli_util::RevisionArg;
24+
use crate::command_error::CommandError;
25+
use crate::complete;
26+
use crate::ui::Ui;
27+
28+
/// Drop a cryptographic signature
29+
#[derive(clap::Args, Clone, Debug)]
30+
pub struct UnsignArgs {
31+
/// What revision(s) to unsign
32+
#[arg(
33+
long, short,
34+
value_name = "REVSETS",
35+
add = ArgValueCandidates::new(complete::mutable_revisions),
36+
)]
37+
revisions: Vec<RevisionArg>,
38+
}
39+
40+
pub fn cmd_unsign(
41+
ui: &mut Ui,
42+
command: &CommandHelper,
43+
args: &UnsignArgs,
44+
) -> Result<(), CommandError> {
45+
let mut workspace_command = command.workspace_helper(ui)?;
46+
47+
let commits: IndexSet<Commit> = workspace_command
48+
.parse_union_revsets(ui, &args.revisions)?
49+
.evaluate_to_commits()?
50+
.try_collect()?;
51+
52+
workspace_command.check_rewritable(commits.iter().ids())?;
53+
54+
let mut tx = workspace_command.start_transaction();
55+
56+
let mut unsigned_commits = vec![];
57+
let mut foreign_commits = vec![];
58+
let mut num_reparented = 0;
59+
let is_authored_by_me =
60+
|commit: &Commit| -> bool { commit.author().email == command.settings().user_email() };
61+
62+
tx.repo_mut().transform_descendants(
63+
commits.iter().ids().cloned().collect_vec(),
64+
|rewriter| {
65+
let old_commit = rewriter.old_commit().clone();
66+
let commit_builder = rewriter.reparent();
67+
68+
if commits.contains(&old_commit) {
69+
let new_commit = commit_builder
70+
.set_sign_behavior(SignBehavior::Drop)
71+
.write()?;
72+
unsigned_commits.push(new_commit.clone());
73+
74+
if old_commit.is_signed() && !is_authored_by_me(&old_commit) {
75+
foreign_commits.push(new_commit);
76+
}
77+
} else {
78+
commit_builder.write()?;
79+
num_reparented += 1;
80+
}
81+
Ok(())
82+
},
83+
)?;
84+
85+
if let Some(mut formatter) = ui.status_formatter() {
86+
match &*unsigned_commits {
87+
[] => {}
88+
[commit] => {
89+
write!(formatter, "Unsigned commit ")?;
90+
tx.base_workspace_helper()
91+
.write_commit_summary(formatter.as_mut(), commit)?;
92+
writeln!(ui.status())?;
93+
}
94+
commits => {
95+
let template = tx.base_workspace_helper().commit_summary_template();
96+
writeln!(formatter, "Unsigned the following commits:")?;
97+
for commit in commits {
98+
write!(formatter, " ")?;
99+
template.format(commit, formatter.as_mut())?;
100+
writeln!(formatter)?;
101+
}
102+
}
103+
};
104+
105+
match &*foreign_commits {
106+
[] => {}
107+
[commit] => {
108+
write!(
109+
ui.warning_default(),
110+
"Unsigned 1 commit not authored by you"
111+
)?;
112+
tx.base_workspace_helper()
113+
.write_commit_summary(formatter.as_mut(), commit)?;
114+
writeln!(ui.status())?;
115+
}
116+
commits => {
117+
let template = tx.base_workspace_helper().commit_summary_template();
118+
writeln!(
119+
ui.warning_default(),
120+
"Unsigned {} commits not authored by you",
121+
commits.len()
122+
)?;
123+
for commit in commits {
124+
write!(formatter, " ")?;
125+
template.format(commit, formatter.as_mut())?;
126+
writeln!(formatter)?;
127+
}
128+
}
129+
};
130+
}
131+
132+
if num_reparented > 0 {
133+
writeln!(ui.status(), "Rebased {num_reparented} descendant commits")?;
134+
}
135+
136+
let transaction_description = match &*unsigned_commits {
137+
[] => "".to_string(),
138+
[commit] => format!("unsign commit {}", commit.id()),
139+
commits => format!(
140+
"unsign commit {} and {} more",
141+
commits[0].id(),
142+
commits.len() - 1
143+
),
144+
};
145+
tx.finish(ui, transaction_description)?;
146+
147+
Ok(())
148+
}

cli/tests/[email protected]

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ This document contains the help content for the `jj` command-line program.
9999
* [`jj util install-man-pages`↴](#jj-util-install-man-pages)
100100
* [`jj util markdown-help`↴](#jj-util-markdown-help)
101101
* [`jj undo`↴](#jj-undo)
102+
* [`jj unsign`↴](#jj-unsign)
102103
* [`jj version`↴](#jj-version)
103104
* [`jj workspace`↴](#jj-workspace)
104105
* [`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/
159160
* `tag` — Manage tags
160161
* `util` — Infrequently used commands such as for generating shell completions
161162
* `undo` — Undo an operation (shortcut for `jj op undo`)
163+
* `unsign` — Drop a cryptographic signature
162164
* `version` — Display version information
163165
* `workspace` — Commands for working with workspaces
164166

@@ -2597,6 +2599,18 @@ Undo an operation (shortcut for `jj op undo`)
25972599

25982600

25992601

2602+
## `jj unsign`
2603+
2604+
Drop a cryptographic signature
2605+
2606+
**Usage:** `jj unsign [OPTIONS]`
2607+
2608+
###### **Options:**
2609+
2610+
* `-r`, `--revisions <REVSETS>` — What revision(s) to unsign
2611+
2612+
2613+
26002614
## `jj version`
26012615

26022616
Display version information

cli/tests/runner.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ mod test_status_command;
7373
mod test_tag_command;
7474
mod test_templater;
7575
mod test_undo;
76+
mod test_unsign_command;
7677
mod test_unsquash_command;
7778
mod test_util_command;
7879
mod test_working_copy;

cli/tests/test_immutable_commits.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -354,4 +354,11 @@ fn test_rewrite_immutable_commands() {
354354
Hint: Could not modify commit: mzvwutvl bcab555f main | (conflict) merge
355355
Hint: Pass `--ignore-immutable` or configure the set of immutable commits via `revset-aliases.immutable_heads()`.
356356
");
357+
// unsign
358+
let stderr = test_env.jj_cmd_failure(&repo_path, &["unsign", "-r=main"]);
359+
insta::assert_snapshot!(stderr, @r"
360+
Error: Commit bcab555fc80e is immutable
361+
Hint: Could not modify commit: mzvwutvl bcab555f main | (conflict) merge
362+
Hint: Pass `--ignore-immutable` or configure the set of immutable commits via `revset-aliases.immutable_heads()`.
363+
");
357364
}

cli/tests/test_unsign_command.rs

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
// Copyright 2023 The Jujutsu Authors
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// https://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
use crate::common::TestEnvironment;
16+
17+
#[test]
18+
fn test_unsign() {
19+
let test_env = TestEnvironment::default();
20+
21+
test_env.add_config(
22+
r#"
23+
[ui]
24+
show-cryptographic-signatures = true
25+
26+
[signing]
27+
behavior = "keep"
28+
backend = "test"
29+
"#,
30+
);
31+
32+
test_env.jj_cmd_ok(test_env.env_root(), &["git", "init", "repo"]);
33+
let repo_path = test_env.env_root().join("repo");
34+
test_env.jj_cmd_ok(&repo_path, &["commit", "-m", "one"]);
35+
test_env.jj_cmd_ok(&repo_path, &["commit", "-m", "two"]);
36+
test_env.jj_cmd_ok(&repo_path, &["commit", "-m", "three"]);
37+
38+
test_env.jj_cmd_ok(&repo_path, &["sign", "-r", "..@-"]);
39+
40+
let stdout = test_env.jj_cmd_success(&repo_path, &["log", "-r", "all()"]);
41+
insta::assert_snapshot!(stdout, @r"
42+
@ zsuskuln [email protected] 2001-02-03 08:05:11 1d35ab70
43+
│ (empty) (no description set)
44+
○ kkmpptxz [email protected] 2001-02-03 08:05:11 0413d103 [✓︎]
45+
│ (empty) three
46+
○ rlvkpnrz [email protected] 2001-02-03 08:05:11 c8768375 [✓︎]
47+
│ (empty) two
48+
○ qpvuntsm [email protected] 2001-02-03 08:05:11 b90f5370 [✓︎]
49+
│ (empty) one
50+
◆ zzzzzzzz root() 00000000
51+
");
52+
53+
let (_, stderr) = test_env.jj_cmd_ok(&repo_path, &["unsign", "-r", "..@-"]);
54+
insta::assert_snapshot!(stderr, @r"
55+
Unsigned the following commits:
56+
qpvuntsm hidden cb05440c (empty) one
57+
rlvkpnrz hidden deb0db4b (empty) two
58+
kkmpptxz hidden 7c11ee12 (empty) three
59+
Rebased 1 descendant commits
60+
Working copy now at: zsuskuln be9daa4d (empty) (no description set)
61+
Parent commit : kkmpptxz 7c11ee12 (empty) three
62+
");
63+
64+
let stdout = test_env.jj_cmd_success(&repo_path, &["log", "-r", "all()"]);
65+
insta::assert_snapshot!(stdout, @r"
66+
@ zsuskuln [email protected] 2001-02-03 08:05:13 be9daa4d
67+
│ (empty) (no description set)
68+
○ kkmpptxz [email protected] 2001-02-03 08:05:13 7c11ee12
69+
│ (empty) three
70+
○ rlvkpnrz [email protected] 2001-02-03 08:05:13 deb0db4b
71+
│ (empty) two
72+
○ qpvuntsm [email protected] 2001-02-03 08:05:13 cb05440c
73+
│ (empty) one
74+
◆ zzzzzzzz root() 00000000
75+
");
76+
}
77+
78+
#[test]
79+
#[should_panic]
80+
fn test_warn_about_unsigning_commits_not_authored_by_me() {
81+
todo!("figure out how to sign commits with different identities in testenv")
82+
}

docs/config.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1196,7 +1196,8 @@ sign-on-push = true
11961196

11971197
### Manually signing commits
11981198

1199-
You can use [`jj sign`](./cli-reference.md#jj-sign) to manually sign commits.
1199+
You can use [`jj sign`](./cli-reference.md#jj-sign)/[`jj unsign`](./cli-reference.md#jj-unsign)
1200+
to sign/unsign commits manually.
12001201

12011202
## Commit Signature Verification
12021203

0 commit comments

Comments
 (0)