-
Notifications
You must be signed in to change notification settings - Fork 365
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Implement jj sign
command
#4747
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,118 @@ | ||
// 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 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::user_error; | ||
use crate::command_error::CommandError; | ||
use crate::complete; | ||
use crate::ui::Ui; | ||
|
||
/// Cryptographically sign a revision | ||
#[derive(clap::Args, Clone, Debug)] | ||
pub struct SignArgs { | ||
/// What key to use, depends on the configured signing backend. | ||
#[arg()] | ||
key: Option<String>, | ||
/// What revision(s) to sign | ||
#[arg( | ||
long, short, | ||
default_value = "@", | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: it might be better to make the default configurable. I think (can be addressed separately) |
||
value_name = "REVSETS", | ||
add = ArgValueCandidates::new(complete::mutable_revisions), | ||
)] | ||
revisions: Vec<RevisionArg>, | ||
/// Sign a commit that is not authored by you or was already signed. | ||
#[arg(long, short)] | ||
force: bool, | ||
Comment on lines
+42
to
+44
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: |
||
/// Drop the signature, explicitly "un-signing" the commit. | ||
#[arg(long, short = 'D', conflicts_with = "force")] | ||
drop: bool, | ||
Comment on lines
+45
to
+47
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: just dropping idea: There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I like |
||
} | ||
|
||
pub fn cmd_sign(ui: &mut Ui, command: &CommandHelper, args: &SignArgs) -> Result<(), CommandError> { | ||
let mut workspace_command = command.workspace_helper(ui)?; | ||
|
||
let commits: Vec<Commit> = workspace_command | ||
.parse_union_revsets(ui, &args.revisions)? | ||
.evaluate_to_commits()? | ||
.try_collect()?; | ||
|
||
let commit_ids = commits.iter().ids().collect_vec(); | ||
workspace_command.check_rewritable(commit_ids)?; | ||
|
||
for commit in &commits { | ||
if !args.force { | ||
if !args.drop && commit.is_signed() { | ||
return Err(user_error( | ||
"Commit is already signed, use --force to sign anyway", | ||
)); | ||
} | ||
if commit.author().email != command.settings().user_email() { | ||
return Err(user_error( | ||
"Commit is not authored by you, use --force to sign anyway", | ||
)); | ||
} | ||
Comment on lines
+63
to
+72
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Now that this version can work with multiple commits, those checks should show the offending one. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: Also |
||
} | ||
} | ||
|
||
let mut tx = workspace_command.start_transaction(); | ||
|
||
let behavior = if args.drop { | ||
SignBehavior::Drop | ||
} else if args.force { | ||
SignBehavior::Force | ||
} else { | ||
SignBehavior::Own | ||
}; | ||
let mut signed_commits = vec![]; | ||
tx.repo_mut().transform_descendants( | ||
commits.iter().ids().cloned().collect_vec(), | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Use Hmm, if |
||
|rewriter| { | ||
if commits.contains(rewriter.old_commit()) { | ||
necauqua marked this conversation as resolved.
Show resolved
Hide resolved
|
||
let commit_builder = rewriter.rebase()?; | ||
let new_commit = commit_builder | ||
.set_sign_key(args.key.clone()) | ||
.set_sign_behavior(behavior) | ||
.write()?; | ||
signed_commits.push(new_commit); | ||
} | ||
Ok(()) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. maybe you'll need to do |
||
}, | ||
)?; | ||
|
||
tx.finish(ui, format!("signed {} commits", signed_commits.len()))?; | ||
|
||
let Some(mut formatter) = ui.status_formatter() else { | ||
return Ok(()); | ||
}; | ||
let template = workspace_command.commit_summary_template(); | ||
for commit in &signed_commits { | ||
if args.drop { | ||
write!(formatter, "Signature was dropped: ")?; | ||
} else { | ||
write!(formatter, "Commit was signed: ")?; | ||
} | ||
Comment on lines
+108
to
+112
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would expect these messages to depend on the commit's previous signature, especially "Signature was dropped" is confusing for commits that were not signed to start with. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This made sense with a singular commit, since pre-check would short-circuit the situation where args.drop=true and the commit was not signed |
||
template.format(commit, formatter.as_mut())?; | ||
writeln!(formatter)?; | ||
} | ||
|
||
Ok(()) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,170 @@ | ||
// 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_sign() { | ||
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"]); | ||
|
||
let template = r#"if(signature, | ||
signature.status() ++ " " ++ signature.display(), | ||
"no" | ||
) ++ " signature""#; | ||
|
||
let show_no_sig = test_env.jj_cmd_success(&repo_path, &["show", "-T", template, "-r", "@-"]); | ||
insta::assert_snapshot!(show_no_sig, @"no signature"); | ||
|
||
let (_, stderr) = test_env.jj_cmd_ok(&repo_path, &["sign", "-r", "@-"]); | ||
insta::assert_snapshot!(stderr, @r" | ||
Rebased 1 descendant commits | ||
Working copy now at: rlvkpnrz 1c141424 (empty) (no description set) | ||
Parent commit : qpvuntsm a9cc7c27 (empty) init | ||
Commit was signed: qpvuntsm a9cc7c27 (empty) init | ||
"); | ||
|
||
let show_with_sig = test_env.jj_cmd_success(&repo_path, &["show", "-T", template, "-r", "@-"]); | ||
insta::assert_snapshot!(show_with_sig, @r"good test-display signature"); | ||
} | ||
|
||
#[test] | ||
fn test_sig_drop() { | ||
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"]); | ||
|
||
let template = r#"if(signature, | ||
signature.status() ++ " " ++ signature.display(), | ||
"no" | ||
) ++ " signature""#; | ||
|
||
let show_no_sig = test_env.jj_cmd_success(&repo_path, &["show", "-T", template, "-r", "@-"]); | ||
insta::assert_snapshot!(show_no_sig, @"no signature"); | ||
|
||
test_env.jj_cmd_ok(&repo_path, &["sign", "-r", "@-"]); | ||
|
||
let show_with_sig = test_env.jj_cmd_success(&repo_path, &["show", "-T", template, "-r", "@-"]); | ||
insta::assert_snapshot!(show_with_sig, @"good test-display signature"); | ||
|
||
let (_, stderr) = test_env.jj_cmd_ok(&repo_path, &["sign", "-r", "@-", "--drop"]); | ||
insta::assert_snapshot!(stderr, @r" | ||
Rebased 1 descendant commits | ||
Working copy now at: rlvkpnrz be42f24a (empty) (no description set) | ||
Parent commit : qpvuntsm 755eae8e (empty) init | ||
Signature was dropped: qpvuntsm 755eae8e (empty) init | ||
"); | ||
|
||
let show_with_sig = test_env.jj_cmd_success(&repo_path, &["show", "-T", template, "-r", "@-"]); | ||
insta::assert_snapshot!(show_with_sig, @"no signature"); | ||
} | ||
|
||
#[test] | ||
fn test_commit_already_signed() { | ||
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, &["sign", "-r", "@-"]); | ||
let stderr = test_env.jj_cmd_failure(&repo_path, &["sign", "-r", "@-"]); | ||
insta::assert_snapshot!(stderr, @"Error: Commit is already signed, use --force to sign anyway"); | ||
} | ||
|
||
#[test] | ||
fn test_force_sign() { | ||
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, &["sign", "-r", "@-"]); | ||
let (_, stderr) = test_env.jj_cmd_ok(&repo_path, &["sign", "--force", "-r", "@-"]); | ||
insta::assert_snapshot!(stderr, @r" | ||
Rebased 1 descendant commits | ||
Working copy now at: rlvkpnrz 1c141424 (empty) (no description set) | ||
Parent commit : qpvuntsm a9cc7c27 (empty) init | ||
Commit was signed: qpvuntsm a9cc7c27 (empty) init | ||
"); | ||
} | ||
|
||
#[test] | ||
fn test_different_author() { | ||
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, | ||
&[ | ||
"desc", | ||
"--author", | ||
"Someone Else <[email protected]>", | ||
"-m", | ||
"init", | ||
"@-", | ||
], | ||
); | ||
let stderr = test_env.jj_cmd_failure(&repo_path, &["sign", "-r", "@-"]); | ||
insta::assert_snapshot!(stderr, @"Error: Commit is not authored by you, use --force to sign anyway"); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -332,7 +332,9 @@ impl DetachedCommitBuilder { | |
} | ||
|
||
pub fn set_sign_key(&mut self, sign_key: Option<String>) -> &mut Self { | ||
self.sign_settings.key = sign_key; | ||
if sign_key.is_some() { | ||
self.sign_settings.key = sign_key; | ||
} | ||
self | ||
} | ||
Comment on lines
334
to
339
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: looking at the overall method now (my previous nit was a mechanical refactor to avoid an unnecessary clone) - |
||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: maybe 2024 or 2025? or maybe this work was started in 2023 so it should be 2023?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I
jj duplicate
d the commit from #3142, so I suppose the work started in 2023.