1use std::io::Write;
25use std::time::{SystemTime, UNIX_EPOCH};
26
27use clap::Parser;
28use mkit_core::index;
29use mkit_core::object::{Commit, Identity, IdentityKind, Object, Tag};
30use mkit_core::refs::{self, Head};
31use mkit_core::serialize;
32use mkit_core::sign::{self, KeyPair};
33use mkit_core::store::ObjectStore;
34use mkit_core::worktree;
35use mkit_keystore::{KeyRef, KeySelector, open_backend};
36
37use crate::clap_shim;
38use crate::config::Config;
39use crate::editor::{COMMIT_EDITMSG_TEMPLATE, spawn_editor};
40use crate::exit;
41use crate::format;
42
43#[derive(Debug, Parser)]
44#[command(
45 name = "mkit commit",
46 about = "Create a signed commit from the staging index."
47)]
48struct CommitOptions {
49 #[arg(short, long)]
51 message: Option<String>,
52 #[arg(long = "author", value_name = "SPEC")]
54 author_spec: Option<String>,
55 #[arg(short = 'a', long)]
58 all: bool,
59 #[arg(long)]
71 amend: bool,
72}
73
74#[must_use]
75#[allow(clippy::too_many_lines)]
76pub fn run(args: &[String]) -> u8 {
77 let normalised = expand_dash_am(args);
80 let opts = match clap_shim::parse::<CommitOptions>("mkit commit", &normalised) {
81 Ok(o) => o,
82 Err(code) => return code,
83 };
84
85 let cwd = match std::env::current_dir() {
86 Ok(p) => p,
87 Err(e) => return emit_err(&format!("cwd: {e}"), exit::NOINPUT),
88 };
89 let store = match super::open_store_configured(&cwd) {
90 Ok(s) => s,
91 Err(e) => return emit_err(&format!("not a mkit repo: {e}"), exit::GENERAL_ERROR),
92 };
93 let mkit_dir = cwd.join(mkit_core::MKIT_DIR);
94 let _lock = match super::acquire_worktree_lock(&cwd) {
95 Ok(l) => l,
96 Err(code) => return code,
97 };
98
99 let cfg = match crate::config::read_or_default(&cwd) {
100 Ok(c) => c,
101 Err(e) => return emit_err(&format!("config: {e}"), exit::CONFIG_ERROR),
102 };
103
104 let amend_target = if opts.amend {
109 match resolve_amend_target(&mkit_dir, &store) {
110 Ok(commit) => Some(commit),
111 Err((m, c)) => return emit_err(&m, c),
112 }
113 } else {
114 None
115 };
116
117 let msg = match opts.message {
121 Some(m) => m,
122 None => match &amend_target {
123 Some(prev) => String::from_utf8_lossy(&prev.message).into_owned(),
124 None => match spawn_editor(COMMIT_EDITMSG_TEMPLATE) {
125 Ok(m) if !m.is_empty() => m,
126 Ok(_) => {
127 return emit_err("empty commit message — aborting", exit::USAGE);
128 }
129 Err(e) => return emit_err(&format!("editor: {e}"), exit::GENERAL_ERROR),
130 },
131 },
132 };
133
134 let mut signer = match load_commit_signer(&cwd, &cfg) {
136 Ok(signer) => signer,
137 Err((msg, code)) => return emit_err(&msg, code),
138 };
139 let signer_public = match signer.public_key() {
140 Ok(public) => public,
141 Err((msg, code)) => return emit_err(&msg, code),
142 };
143
144 let author = match resolve_author(
147 opts.author_spec.as_deref(),
148 &cfg.user_identity,
149 &signer_public,
150 ) {
151 Ok(id) => id,
152 Err(e) => return emit_err(&format!("author: {e}"), exit::CONFIG_ERROR),
153 };
154
155 if opts.all
156 && let Err(e) = super::add::stage_tracked_changes(&cwd, &store)
157 {
158 return emit_err(&format!("stage tracked changes: {e}"), exit::GENERAL_ERROR);
159 }
160
161 let idx = match index::read_index(&cwd) {
168 Ok(idx) => idx,
169 Err(e) => return emit_err(&format!("read index: {e}"), exit::GENERAL_ERROR),
170 };
171 if idx.entries.is_empty() {
172 return emit_err(
173 "nothing staged: index is empty; run `mkit add <path>` (or `mkit add .`) before commit",
174 exit::USAGE,
175 );
176 }
177 let batch = store.batch();
181 let tree_hash = match worktree::build_tree_from_index_with(&store, &batch, &idx, true) {
184 Ok(h) => h,
185 Err(e) => return emit_err(&format!("build tree: {e}"), exit::GENERAL_ERROR),
186 };
187 let parents = match &amend_target {
191 Some(prev) => prev.parents.clone(),
192 None => match refs::resolve_head(&mkit_dir) {
193 Ok(Some(h)) => vec![h],
194 _ => vec![],
195 },
196 };
197 let timestamp = SystemTime::now()
198 .duration_since(UNIX_EPOCH)
199 .map_or(0, |d| d.as_secs());
200 let mut unsigned = Commit::new_unannotated(
201 tree_hash,
202 parents,
203 author,
204 signer_public,
205 msg.as_bytes().to_vec(),
206 timestamp,
207 [0u8; 64],
208 );
209 let sig = match signer.sign_commit(&unsigned) {
210 Ok(s) => s,
211 Err((msg, code)) => return emit_err(&msg, code),
212 };
213 unsigned.signature = sig;
214 let bytes = match serialize::serialize(&Object::Commit(unsigned)) {
215 Ok(b) => b,
216 Err(e) => return emit_err(&format!("serialize commit: {e}"), exit::DATAERR),
217 };
218 let commit_hash = match batch.write(&bytes) {
219 Ok(h) => h,
220 Err(e) => return emit_err(&format!("store commit: {e}"), exit::CANTCREAT),
221 };
222 if let Err(e) = batch.commit() {
225 return emit_err(&format!("store commit: {e}"), exit::CANTCREAT);
226 }
227 if amend_target.is_some() {
231 match refs::resolve_head(&mkit_dir) {
232 Ok(Some(old_head)) => {
233 let branch = super::head_branch_name(&mkit_dir);
234 if let Err((m, c)) = super::record_superseded(&mkit_dir, "amend", &branch, old_head)
235 {
236 return emit_err(&m, c);
237 }
238 }
239 Ok(None) => {}
240 Err(e) => return emit_err(&format!("read HEAD: {e}"), exit::DATAERR),
241 }
242 }
243 if let Err((m, c)) = advance_head(&mkit_dir, &commit_hash) {
244 return emit_err(&m, c);
245 }
246 if let Err(e) = super::sync_index_to_tree(&cwd, &store, tree_hash) {
247 return emit_err(&e, exit::CANTCREAT);
248 }
249 let mut stderr = std::io::stderr().lock();
250 let verb = if amend_target.is_some() {
251 "amended"
252 } else {
253 "committed"
254 };
255 let _ = writeln!(
256 stderr,
257 "{verb} {} ({})",
258 format::short_hash(&commit_hash, 8),
259 msg.lines().next().unwrap_or("")
260 );
261 exit::OK
262}
263
264fn expand_dash_am(args: &[String]) -> Vec<String> {
268 let mut out: Vec<String> = Vec::with_capacity(args.len() + 2);
269 let mut iter = args.iter();
270 while let Some(a) = iter.next() {
271 match a.as_str() {
272 "-am" => {
273 out.push("-a".to_owned());
274 out.push("-m".to_owned());
275 if let Some(next) = iter.next() {
276 out.push(next.clone());
277 }
278 }
279 s if s.starts_with("-am") && s.len() > 3 => {
280 out.push("-a".to_owned());
281 out.push("-m".to_owned());
282 out.push(s[3..].to_owned());
283 }
284 _ => out.push(a.clone()),
285 }
286 }
287 out
288}
289
290#[cfg(test)]
291mod expand_dash_am_tests {
292 use super::expand_dash_am;
293
294 fn to_strs(args: &[String]) -> Vec<&str> {
295 args.iter().map(String::as_str).collect()
296 }
297
298 #[test]
299 fn fused_dash_am_with_inline_message() {
300 let out = expand_dash_am(&["-amhello".to_owned()]);
301 assert_eq!(to_strs(&out), &["-a", "-m", "hello"]);
302 }
303
304 #[test]
305 fn spaced_dash_am_with_following_message() {
306 let out = expand_dash_am(&["-am".to_owned(), "hello".to_owned()]);
307 assert_eq!(to_strs(&out), &["-a", "-m", "hello"]);
308 }
309
310 #[test]
311 fn unrelated_args_pass_through() {
312 let out = expand_dash_am(&[
313 "-m".to_owned(),
314 "msg".to_owned(),
315 "--author".to_owned(),
316 "id".to_owned(),
317 ]);
318 assert_eq!(to_strs(&out), &["-m", "msg", "--author", "id"]);
319 }
320}
321
322fn load_signing_key(
334 cwd: &std::path::Path,
335 rel_signing_key_path: &str,
336) -> Result<KeyPair, (String, u8)> {
337 let key_path = match crate::config::resolve_key_path(cwd, rel_signing_key_path) {
338 Ok(p) => p,
339 Err(e) => return Err((format!("{e}"), exit::CONFIG_ERROR)),
340 };
341 if !key_path.exists() {
342 return Err((
343 format!(
344 "no signing key at {} — run `mkit keygen` to create one",
345 key_path.display()
346 ),
347 exit::NOINPUT,
348 ));
349 }
350 sign::load_key(&key_path).map_err(|e| (format!("load key: {e}"), exit::NOPERM))
351}
352
353pub(super) enum CommitSigner {
354 Legacy(KeyPair),
355 Keystore(Box<dyn mkit_keystore::KeySigner>),
356}
357
358impl CommitSigner {
359 pub(super) fn public_key(&self) -> Result<[u8; 32], (String, u8)> {
360 match self {
361 Self::Legacy(kp) => Ok(kp.public.0),
362 Self::Keystore(signer) => {
363 let public = signer
364 .public_key()
365 .map_err(|error| (format!("keystore public key: {error}"), exit::DATAERR))?;
366 public.as_bytes().try_into().map_err(|_| {
367 (
368 format!(
369 "keystore Ed25519 public key must be 32 bytes, got {}",
370 public.len()
371 ),
372 exit::DATAERR,
373 )
374 })
375 }
376 }
377 }
378
379 pub(super) fn sign_tag(&mut self, tag: &Tag) -> Result<[u8; 64], (String, u8)> {
383 match self {
384 Self::Legacy(kp) => sign::sign_tag(tag, kp)
385 .map(|signature| signature.0)
386 .map_err(|error| (format!("sign: {error}"), exit::GENERAL_ERROR)),
387 Self::Keystore(signer) => {
388 let digest = sign::tag_signing_hash(tag)
389 .map_err(|error| (format!("tag signing hash: {error}"), exit::DATAERR))?;
390 let signature = signer
391 .sign(&digest)
392 .map_err(|error| (format!("keystore sign: {error}"), exit::DATAERR))?;
393 signature.try_into().map_err(|signature: Vec<u8>| {
394 (
395 format!(
396 "keystore Ed25519 signature must be 64 bytes, got {}",
397 signature.len()
398 ),
399 exit::DATAERR,
400 )
401 })
402 }
403 }
404 }
405
406 pub(super) fn sign_commit(&mut self, commit: &Commit) -> Result<[u8; 64], (String, u8)> {
407 match self {
408 Self::Legacy(kp) => sign::sign_commit(commit, kp)
409 .map(|signature| signature.0)
410 .map_err(|error| (format!("sign: {error}"), exit::GENERAL_ERROR)),
411 Self::Keystore(signer) => {
412 let digest = sign::commit_signing_hash(commit)
413 .map_err(|error| (format!("commit signing hash: {error}"), exit::DATAERR))?;
414 let signature = signer
415 .sign(&digest)
416 .map_err(|error| (format!("keystore sign: {error}"), exit::DATAERR))?;
417 signature.try_into().map_err(|signature: Vec<u8>| {
418 (
419 format!(
420 "keystore Ed25519 signature must be 64 bytes, got {}",
421 signature.len()
422 ),
423 exit::DATAERR,
424 )
425 })
426 }
427 }
428 }
429}
430
431pub(super) fn load_commit_signer(
432 cwd: &std::path::Path,
433 cfg: &Config,
434) -> Result<CommitSigner, (String, u8)> {
435 match cfg.signer.as_str() {
436 "" | "legacy" => load_signing_key(cwd, &cfg.signing_key).map(CommitSigner::Legacy),
437 "keystore" => load_keystore_commit_signer(cfg),
438 other => Err((
439 format!("unknown signer `{other}` — expected `legacy` or `keystore`"),
440 exit::CONFIG_ERROR,
441 )),
442 }
443}
444
445fn load_keystore_commit_signer(cfg: &Config) -> Result<CommitSigner, (String, u8)> {
446 let key_ref = cfg
447 .key
448 .ed25519_ref_or_fallback()
449 .parse::<KeyRef>()
450 .map_err(|error| (format!("key.ed25519_ref: {error}"), exit::CONFIG_ERROR))?;
451 let store = open_backend(key_ref.backend())
452 .map_err(|error| (format!("keystore backend: {error}"), exit::UNAVAILABLE))?;
453 let selector = KeySelector::new(
454 key_ref.label().to_owned(),
455 Some(mkit_keystore::Algorithm::Ed25519),
456 )
457 .map_err(|error| (format!("key.ed25519_ref: {error}"), exit::CONFIG_ERROR))?;
458 let opener = store.opener().ok_or_else(|| {
459 (
460 format!(
461 "keystore backend `{}` does not support opening keys",
462 key_ref.backend()
463 ),
464 exit::DATAERR,
465 )
466 })?;
467 let signer = opener.open(&selector).map_err(|error| match error {
468 mkit_keystore::Error::KeyNotFound(_) => (
469 format!(
470 "missing keystore signing key for algorithm ed25519 — run `mkit key generate --backend {} --algorithm ed25519 --label <label>` first, or set `signer = legacy` and use `mkit keygen`: {error}",
471 key_ref.backend()
472 ),
473 exit::NOINPUT,
474 ),
475 other => (
476 format!("keystore signing key for algorithm ed25519: {other}"),
477 exit::DATAERR,
478 ),
479 })?;
480 Ok(CommitSigner::Keystore(signer))
481}
482
483fn resolve_amend_target(
490 mkit_dir: &std::path::Path,
491 store: &ObjectStore,
492) -> Result<Commit, (String, u8)> {
493 let head = refs::resolve_head(mkit_dir)
494 .map_err(|e| (format!("read HEAD: {e}"), exit::DATAERR))?
495 .ok_or_else(|| {
496 (
497 "nothing to amend: HEAD has no commit yet".to_owned(),
498 exit::USAGE,
499 )
500 })?;
501 match store.read_object(&head) {
502 Ok(Object::Commit(c)) => Ok(c),
503 Ok(_) => Err((
504 format!(
505 "cannot amend: HEAD {} is not a commit",
506 format::hex_hash(&head)
507 ),
508 exit::DATAERR,
509 )),
510 Err(e) => Err((
511 format!("read HEAD commit {}: {e}", format::hex_hash(&head)),
512 exit::DATAERR,
513 )),
514 }
515}
516
517fn advance_head(
526 mkit_dir: &std::path::Path,
527 commit_hash: &mkit_core::hash::Hash,
528) -> Result<(), (String, u8)> {
529 let head = refs::read_head(mkit_dir).map_err(|e| (format!("read HEAD: {e}"), exit::DATAERR))?;
530 match head {
531 Head::Branch(name) => super::write_ref_recording_history(
532 mkit_dir,
533 &name,
534 refs::RefWriteCondition::Any,
535 commit_hash,
536 )
537 .map_err(|e| (format!("write ref: {e}"), exit::CANTCREAT)),
538 Head::Detached(_) => refs::write_head_detached(mkit_dir, commit_hash)
539 .map_err(|e| (format!("update HEAD: {e}"), exit::CANTCREAT)),
540 }
541}
542
543pub(super) fn resolve_author(
550 author_flag: Option<&str>,
551 cfg_user_identity: &str,
552 signer_public: &[u8; 32],
553) -> Result<Identity, String> {
554 if let Some(spec) = author_flag {
555 return parse_author_spec(spec);
556 }
557 if !cfg_user_identity.is_empty() {
558 return decode_user_identity_hex(cfg_user_identity);
559 }
560 Ok(Identity::ed25519(*signer_public))
561}
562
563fn parse_author_spec(spec: &str) -> Result<Identity, String> {
573 if let Some(hex) = spec.strip_prefix("ed25519:") {
574 let bytes = hex_decode(hex).ok_or_else(|| "ed25519:<hex> invalid hex".to_string())?;
575 if bytes.len() != 32 {
576 return Err("ed25519:<hex> must decode to 32 bytes".to_string());
577 }
578 let mut arr = [0u8; 32];
579 arr.copy_from_slice(&bytes);
580 return Ok(Identity::ed25519(arr));
581 }
582 if let Some(payload) = spec.strip_prefix("did:key:") {
583 let id = Identity {
588 kind: IdentityKind::DidKey,
589 bytes: payload.as_bytes().to_vec(),
590 };
591 if !id.is_valid() {
592 return Err(
593 "did:key:<multibase> must be a non-empty printable-ASCII multibase string \
594 (e.g. did:key:z6Mk…)"
595 .to_string(),
596 );
597 }
598 return Ok(id);
599 }
600 if let Some(raw) = spec.strip_prefix("opaque:") {
601 if raw.is_empty() {
602 return Err("opaque:<bytes> must not be empty".to_string());
603 }
604 return Ok(Identity::opaque(raw.as_bytes().to_vec()));
605 }
606 Err(format!(
607 "unknown identity spec '{spec}' — expected ed25519:<hex>, did:key:<hex>, or opaque:<bytes>"
608 ))
609}
610
611fn decode_user_identity_hex(hex: &str) -> Result<Identity, String> {
615 let bytes =
616 hex_decode(hex).ok_or_else(|| "user.identity: not a lowercase hex string".to_string())?;
617 if bytes.len() < 3 {
618 return Err("user.identity: too short (kind + len prefix missing)".to_string());
619 }
620 let kind_byte = bytes[0];
621 let declared_len = u16::from(bytes[1]) | (u16::from(bytes[2]) << 8);
622 if bytes.len() != usize::from(declared_len) + 3 {
623 return Err("user.identity: declared length does not match payload".to_string());
624 }
625 let payload = bytes[3..].to_vec();
626 let kind = match kind_byte {
627 0x01 => IdentityKind::Ed25519,
628 0x02 => IdentityKind::DidKey,
629 0x03 | 0x04 => IdentityKind::Opaque,
631 other => return Err(format!("user.identity: unknown kind byte {other:#04x}")),
632 };
633 if kind == IdentityKind::Ed25519 && payload.len() != 32 {
634 return Err("user.identity: ed25519 payload must be exactly 32 bytes".to_string());
635 }
636 Ok(Identity {
637 kind,
638 bytes: payload,
639 })
640}
641
642fn hex_decode(s: &str) -> Option<Vec<u8>> {
643 if !s.len().is_multiple_of(2) {
644 return None;
645 }
646 let mut out = Vec::with_capacity(s.len() / 2);
647 let b = s.as_bytes();
648 let mut i = 0;
649 while i < b.len() {
650 let hi = nibble(b[i])?;
651 let lo = nibble(b[i + 1])?;
652 out.push((hi << 4) | lo);
653 i += 2;
654 }
655 Some(out)
656}
657
658fn nibble(c: u8) -> Option<u8> {
659 Some(match c {
660 b'0'..=b'9' => c - b'0',
661 b'a'..=b'f' => 10 + c - b'a',
662 b'A'..=b'F' => 10 + c - b'A',
663 _ => return None,
664 })
665}
666
667fn emit_err(msg: &str, code: u8) -> u8 {
668 let mut stderr = std::io::stderr().lock();
669 let _ = writeln!(stderr, "error: {msg}");
670 code
671}
672
673#[cfg(test)]
674mod tests {
675 use super::*;
676 use mkit_keystore::Keystore;
677
678 #[test]
679 fn parse_author_ed25519_roundtrips() {
680 let hex = "11".repeat(32);
681 let spec = format!("ed25519:{hex}");
682 let id = parse_author_spec(&spec).unwrap();
683 assert_eq!(id.kind, IdentityKind::Ed25519);
684 assert_eq!(id.bytes.len(), 32);
685 assert!(id.bytes.iter().all(|&b| b == 0x11));
686 }
687
688 #[test]
689 fn parse_author_rejects_bad_ed25519() {
690 assert!(parse_author_spec("ed25519:short").is_err());
691 assert!(parse_author_spec("ed25519:zzzzz").is_err());
692 }
693
694 #[test]
695 fn parse_author_did_key_stores_multibase_payload() {
696 let id = parse_author_spec("did:key:z6MkExample").unwrap();
698 assert_eq!(id.kind, IdentityKind::DidKey);
699 assert_eq!(id.bytes, b"z6MkExample");
700 assert!(id.is_valid());
701 }
702
703 #[test]
704 fn parse_author_did_key_rejects_non_multibase() {
705 assert!(parse_author_spec("did:key:").is_err());
708 assert!(parse_author_spec("did:key:has space").is_err());
709 }
710
711 #[test]
712 fn parse_author_opaque_takes_raw_bytes() {
713 let id = parse_author_spec("opaque:hello world").unwrap();
714 assert_eq!(id.kind, IdentityKind::Opaque);
715 assert_eq!(id.bytes, b"hello world");
716 }
717
718 #[test]
719 fn parse_author_rejects_unknown_prefix() {
720 assert!(parse_author_spec("foo:bar").is_err());
721 assert!(parse_author_spec("").is_err());
722 }
723
724 #[test]
725 fn decode_user_identity_ed25519_roundtrip() {
726 let mut hex = String::from("012000");
729 hex.push_str(&"ab".repeat(32));
730 let id = decode_user_identity_hex(&hex).unwrap();
731 assert_eq!(id.kind, IdentityKind::Ed25519);
732 assert_eq!(id.bytes.len(), 32);
733 }
734
735 #[test]
736 fn decode_user_identity_rejects_length_mismatch() {
737 let hex = "011000aabbcc"; assert!(decode_user_identity_hex(hex).is_err());
739 }
740
741 #[test]
742 fn resolve_author_prefers_flag_over_config() {
743 let kp = KeyPair::generate().unwrap();
744 let hex = "22".repeat(32);
745 let spec = format!("ed25519:{hex}");
746 let cfg_hex = {
748 let mut s = String::from("012000");
749 s.push_str(&"33".repeat(32));
750 s
751 };
752 let id = resolve_author(Some(&spec), &cfg_hex, &kp.public.0).unwrap();
753 assert!(id.bytes.iter().all(|&b| b == 0x22));
754 }
755
756 #[test]
757 fn resolve_author_uses_config_when_no_flag() {
758 let kp = KeyPair::generate().unwrap();
759 let mut cfg_hex = String::from("012000");
760 cfg_hex.push_str(&"44".repeat(32));
761 let id = resolve_author(None, &cfg_hex, &kp.public.0).unwrap();
762 assert_eq!(id.kind, IdentityKind::Ed25519);
763 assert!(id.bytes.iter().all(|&b| b == 0x44));
764 }
765
766 #[test]
767 fn resolve_author_falls_back_to_pubkey() {
768 let kp = KeyPair::generate().unwrap();
769 let id = resolve_author(None, "", &kp.public.0).unwrap();
770 assert_eq!(id.kind, IdentityKind::Ed25519);
771 assert_eq!(id.bytes, kp.public.0.to_vec());
772 }
773
774 #[test]
775 fn keystore_commit_signature_matches_legacy_keypair_signature() {
776 let seed = [0x5a; 32];
777 let kp = KeyPair::from_seed(seed);
778 let store_root = tempfile::tempdir().unwrap();
779 let store = mkit_keystore::SoftwareRawKeystore::with_root(store_root.path().join("keys"));
780 store
781 .importer()
782 .unwrap()
783 .import(
784 &mkit_keystore::KeyLabel::new("committer").unwrap(),
785 mkit_keystore::SecretKey::new(mkit_keystore::Algorithm::Ed25519, seed),
786 mkit_keystore::KeyAttrs::default(),
787 mkit_keystore::ImportOptions::default(),
788 )
789 .unwrap();
790 let selector =
791 mkit_keystore::KeySelector::new("committer", Some(mkit_keystore::Algorithm::Ed25519))
792 .unwrap();
793 let mut signer = CommitSigner::Keystore(store.opener().unwrap().open(&selector).unwrap());
794 let signer_public = signer.public_key().unwrap();
795 let commit = Commit::new_unannotated(
796 [1; 32],
797 vec![[2; 32]],
798 Identity::ed25519(signer_public),
799 signer_public,
800 b"same commit".to_vec(),
801 123,
802 [0; 64],
803 );
804
805 let keystore_sig = signer.sign_commit(&commit).unwrap();
806 let legacy_sig = sign::sign_commit(&commit, &kp).unwrap().0;
807 assert_eq!(keystore_sig, legacy_sig);
808 }
809}