// 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 as _; use jj_lib::commit::Commit; use jj_lib::commit::CommitIteratorExt as _; use jj_lib::repo::Repo as _; use jj_lib::signing::SignBehavior; use crate::cli_util::print_updated_commits; use crate::cli_util::CommandHelper; use crate::cli_util::RevisionArg; use crate::command_error::user_error_with_hint; use crate::command_error::CommandError; use crate::complete; use crate::ui::Ui; /// Cryptographically sign a revision /// /// This command requires configuring a [commit signing] backend. /// /// [commit signing]: /// https://jj-vcs.github.io/jj/latest/config/#commit-signing #[derive(clap::Args, Clone, Debug)] pub struct SignArgs { /// What revision(s) to sign /// /// If no revisions are specified, this defaults to the `revsets.sign` /// setting. /// /// Note that revisions are always re-signed. /// /// While that leads to discomfort for users, which sign with hardware /// devices, as of now we cannot reliably check if a commit is already /// signed by the user without creating a signature (see [#5786]). /// /// [#5786]: /// https://github.com/jj-vcs/jj/issues/5786 #[arg( long, short, value_name = "REVSETS", add = ArgValueCandidates::new(complete::mutable_revisions), )] revisions: Vec, /// The key used for signing #[arg(long)] key: Option, } pub fn cmd_sign(ui: &mut Ui, command: &CommandHelper, args: &SignArgs) -> Result<(), CommandError> { let mut workspace_command = command.workspace_helper(ui)?; if !workspace_command.repo().store().signer().can_sign() { return Err(user_error_with_hint( "No signing backend configured", "For configuring a signing backend, see https://jj-vcs.github.io/jj/latest/config/#commit-signing", )); } let revset_expression = if args.revisions.is_empty() { let revset_string = workspace_command.settings().get_string("revsets.sign")?; workspace_command.parse_revset(ui, &RevisionArg::from(revset_string))? } else { workspace_command.parse_union_revsets(ui, &args.revisions)? }; let to_sign: IndexSet = revset_expression.evaluate_to_commits()?.try_collect()?; workspace_command.check_rewritable(to_sign.iter().ids())?; let mut tx = workspace_command.start_transaction(); let mut signed_commits = vec![]; let mut num_reparented = 0; tx.repo_mut().transform_descendants( to_sign.iter().ids().cloned().collect_vec(), |rewriter| { let old_commit = rewriter.old_commit().clone(); let mut commit_builder = rewriter.reparent(); if to_sign.contains(&old_commit) { if let Some(key) = &args.key { commit_builder = commit_builder.set_sign_key(key.clone()); } let new_commit = commit_builder .set_sign_behavior(SignBehavior::Force) .write()?; signed_commits.push(new_commit); } else { commit_builder.write()?; num_reparented += 1; } Ok(()) }, )?; if let Some(mut formatter) = ui.status_formatter() { if !signed_commits.is_empty() { writeln!(formatter, "Signed {} commits:", signed_commits.len())?; print_updated_commits( formatter.as_mut(), &tx.commit_summary_template(), &signed_commits, )?; } } let num_not_authored_by_me = signed_commits .iter() .filter(|commit| commit.author().email != tx.settings().user_email()) .count(); if num_not_authored_by_me > 0 { writeln!( ui.warning_default(), "{num_not_authored_by_me} of these commits are not authored by you", )?; } if num_reparented > 0 { writeln!(ui.status(), "Rebased {num_reparented} descendant commits")?; } let transaction_description = match &*signed_commits { [] => "".to_string(), [commit] => format!("sign commit {}", commit.id()), commits => format!( "sign commit {} and {} more", commits[0].id(), commits.len() - 1 ), }; tx.finish(ui, transaction_description)?; Ok(()) }