@@ -3,7 +3,10 @@ use std::fs;
33/// These tests cover the `sign_mode` property on the Step::Pick.
44use anyhow:: Result ;
55use but_graph:: Graph ;
6- use but_rebase:: graph_rebase:: { Editor , Pick , Step , cherry_pick:: PickSignMode } ;
6+ use but_rebase:: graph_rebase:: {
7+ Editor , GraphEditorOptions , Pick , Step ,
8+ cherry_pick:: { PickSignGuard , PickSignMode } ,
9+ } ;
710use but_testsupport:: { cat_commit, visualize_commit_graph_all} ;
811
912use crate :: utils:: { fixture_writable_with_signing, standard_options} ;
@@ -185,3 +188,362 @@ fn when_cherry_picking_dont_resign_if_not_set() -> Result<()> {
185188
186189 Ok ( ( ) )
187190}
191+
192+ /// Picking with [`PickSignMode::Force`] should cause the pick to be cherry-picked even in
193+ /// absence of other changes, and the resulting commit should be signed.
194+ #[ test]
195+ fn commit_picked_with_force_sign_is_signed_when_otherwise_unchanged_and_signing_config_is_not_enabled ( )
196+ -> Result < ( ) > {
197+ let ( repo, _tmpdir, mut meta) = fixture_writable_with_signing (
198+ "unsigned-commits-with-signing-key-setup-but-signing-disabled" ,
199+ ) ?;
200+
201+ let before = visualize_commit_graph_all ( & repo) ?;
202+ insta:: assert_snapshot!( before, @"
203+ * ea8caac (HEAD -> main, top) top
204+ * 135e6ba (mid) mid
205+ * 7a5aacf (base) base
206+ " ) ;
207+
208+ let graph = Graph :: from_head ( & repo, & * meta, standard_options ( ) ) ?. validated ( ) ?;
209+ let mut ws = graph. into_workspace ( ) ?;
210+ let mut editor = Editor :: create_with_opts (
211+ & mut ws,
212+ & mut * meta,
213+ & repo,
214+ & GraphEditorOptions {
215+ sign_mode : PickSignMode :: Never ,
216+ } ,
217+ ) ?;
218+
219+ // Force sign the top commit
220+ let top_commit_id = repo. rev_parse_single ( "top" ) ?. detach ( ) ;
221+ let top_commit_sel = editor. select_commit ( top_commit_id) ?;
222+ let mut pick = Pick :: new_pick ( top_commit_id) ;
223+ pick. sign_mode = PickSignMode :: Force ;
224+ editor. replace ( top_commit_sel, Step :: Pick ( pick) ) ?;
225+
226+ let outcome = editor. rebase ( ) ?;
227+ let materialize_outcome = outcome. materialize ( ) ?;
228+
229+ let after = visualize_commit_graph_all ( & repo) ?;
230+ insta:: assert_snapshot!( after, @"
231+ * 94b4c45 (HEAD -> main, top) top
232+ * 135e6ba (mid) mid
233+ * 7a5aacf (base) base
234+ " ) ;
235+
236+ let commit_mappings = materialize_outcome. history . commit_mappings ( ) ;
237+ assert_eq ! (
238+ commit_mappings. len( ) ,
239+ 1 ,
240+ "expected 1 commit to be cherry-picked"
241+ ) ;
242+ let new_commit_id = commit_mappings
243+ . get ( & top_commit_id)
244+ . expect ( "the force-signed commit should be in the commit mappings" ) ;
245+
246+ let new_commit = repo. find_commit ( * new_commit_id) ?;
247+ assert ! (
248+ new_commit
249+ . decode( ) ?
250+ . extra_headers( )
251+ . pgp_signature( )
252+ . is_some( ) ,
253+ "expected the force-signed commit to be signed"
254+ ) ;
255+
256+ Ok ( ( ) )
257+ }
258+
259+ /// Picking with [`PickSignMode::Force`] should _not_ cause a cascade of signatures on
260+ /// descendants that are picked with [`PickSignMode::Never`].
261+ #[ test]
262+ fn force_signing_ancestor_does_not_sign_descendants_that_are_picked_with_sign_mode_never ( )
263+ -> Result < ( ) > {
264+ let ( repo, _tmpdir, mut meta) = fixture_writable_with_signing (
265+ "unsigned-commits-with-signing-key-setup-but-signing-disabled" ,
266+ ) ?;
267+
268+ let before = visualize_commit_graph_all ( & repo) ?;
269+ insta:: assert_snapshot!( before, @"
270+ * ea8caac (HEAD -> main, top) top
271+ * 135e6ba (mid) mid
272+ * 7a5aacf (base) base
273+ " ) ;
274+
275+ let graph = Graph :: from_head ( & repo, & * meta, standard_options ( ) ) ?. validated ( ) ?;
276+ let mut ws = graph. into_workspace ( ) ?;
277+ let mut editor = Editor :: create_with_opts (
278+ & mut ws,
279+ & mut * meta,
280+ & repo,
281+ & GraphEditorOptions {
282+ sign_mode : PickSignMode :: Never ,
283+ } ,
284+ ) ?;
285+
286+ let top_commit_id = repo. rev_parse_single ( "top" ) ?. detach ( ) ;
287+ let mid_commit_id = repo. rev_parse_single ( "mid" ) ?. detach ( ) ;
288+
289+ // We pick the mid commit with force. This should cause it to be signed, but its descendant
290+ // top commit should _not_ get signed as it was picked with PickSignMode::Never.
291+ let mid_sel = editor. select_commit ( mid_commit_id) ?;
292+ let mut pick = Pick :: new_pick ( mid_commit_id) ;
293+ pick. sign_mode = PickSignMode :: Force ;
294+ editor. replace ( mid_sel, Step :: Pick ( pick) ) ?;
295+
296+ let outcome = editor. rebase ( ) ?;
297+ let materialize_outcome = outcome. materialize ( ) ?;
298+
299+ let after = visualize_commit_graph_all ( & repo) ?;
300+ insta:: assert_snapshot!( after, @"
301+ * 5f964cc (HEAD -> main, top) top
302+ * 9320e55 (mid) mid
303+ * 7a5aacf (base) base
304+ " ) ;
305+
306+ let commit_mappings = materialize_outcome. history . commit_mappings ( ) ;
307+ assert_eq ! (
308+ commit_mappings. len( ) ,
309+ 2 ,
310+ "expected 2 commits to be cherry-picked"
311+ ) ;
312+ let new_mid_commit_id = commit_mappings
313+ . get ( & mid_commit_id)
314+ . expect ( "the force-signed commit should be in the commit mappings" ) ;
315+ let new_top_commit_id = commit_mappings
316+ . get ( & top_commit_id)
317+ . expect ( "the head commit should be in the commit mappings" ) ;
318+
319+ let new_top_commit = repo. find_commit ( * new_top_commit_id) ?;
320+ let new_mid_commit = repo. find_commit ( * new_mid_commit_id) ?;
321+ assert ! (
322+ new_top_commit
323+ . decode( ) ?
324+ . extra_headers( )
325+ . pgp_signature( )
326+ . is_none( ) ,
327+ "top commit should not have been cascade-signed"
328+ ) ;
329+ assert ! (
330+ new_mid_commit
331+ . decode( ) ?
332+ . extra_headers( )
333+ . pgp_signature( )
334+ . is_some( ) ,
335+ "mid commit should have been force-signed"
336+ ) ;
337+
338+ Ok ( ( ) )
339+ }
340+
341+ /// Picking with with [`PickSignMode::Force`] _should_ cause a cascade of signatures when
342+ /// descendants are picked with an unguarded [`PickSignMode::IfChanged`].
343+ ///
344+ /// This is the primary mechanism by which we can programmatically sign/re-sign a branch
345+ /// independently of Git-compatible configuration.
346+ #[ test]
347+ fn force_signing_ancestor_triggers_cascading_signatures_of_descendants_that_are_picked_with_unguarded_sign_mode_ifchanged ( )
348+ -> Result < ( ) > {
349+ let ( repo, _tmpdir, mut meta) = fixture_writable_with_signing (
350+ "unsigned-commits-with-signing-key-setup-but-signing-disabled" ,
351+ ) ?;
352+
353+ let before = visualize_commit_graph_all ( & repo) ?;
354+ insta:: assert_snapshot!( before, @"
355+ * ea8caac (HEAD -> main, top) top
356+ * 135e6ba (mid) mid
357+ * 7a5aacf (base) base
358+ " ) ;
359+
360+ let graph = Graph :: from_head ( & repo, & * meta, standard_options ( ) ) ?. validated ( ) ?;
361+ let mut ws = graph. into_workspace ( ) ?;
362+ let mut editor = Editor :: create_with_opts (
363+ & mut ws,
364+ & mut * meta,
365+ & repo,
366+ & GraphEditorOptions {
367+ sign_mode : PickSignMode :: IfChanged ( PickSignGuard :: None ) ,
368+ } ,
369+ ) ?;
370+
371+ let top_commit_id = repo. rev_parse_single ( "top" ) ?. detach ( ) ;
372+ let mid_commit_id = repo. rev_parse_single ( "mid" ) ?. detach ( ) ;
373+
374+ // We pick the mid commit with force. This should cause it to be signed, and its descendant
375+ // top commit should get signed through the cascading rewrites.
376+ let mid_sel = editor. select_commit ( mid_commit_id) ?;
377+ let mut pick = Pick :: new_pick ( mid_commit_id) ;
378+ pick. sign_mode = PickSignMode :: Force ;
379+ editor. replace ( mid_sel, Step :: Pick ( pick) ) ?;
380+
381+ let outcome = editor. rebase ( ) ?;
382+ let materialize_outcome = outcome. materialize ( ) ?;
383+
384+ let after = visualize_commit_graph_all ( & repo) ?;
385+ insta:: assert_snapshot!( after, @"
386+ * 4d0bf76 (HEAD -> main, top) top
387+ * 9320e55 (mid) mid
388+ * 7a5aacf (base) base
389+ " ) ;
390+
391+ let commit_mappings = materialize_outcome. history . commit_mappings ( ) ;
392+ assert_eq ! (
393+ commit_mappings. len( ) ,
394+ 2 ,
395+ "expected 2 commits to be cherry-picked"
396+ ) ;
397+ let new_mid_commit_id = commit_mappings
398+ . get ( & mid_commit_id)
399+ . expect ( "the force-signed commit should be in the commit mappings" ) ;
400+ let new_top_commit_id = commit_mappings
401+ . get ( & top_commit_id)
402+ . expect ( "the head commit should be in the commit mappings" ) ;
403+
404+ let new_top_commit = repo. find_commit ( * new_top_commit_id) ?;
405+ let new_mid_commit = repo. find_commit ( * new_mid_commit_id) ?;
406+ assert ! (
407+ new_mid_commit
408+ . decode( ) ?
409+ . extra_headers( )
410+ . pgp_signature( )
411+ . is_some( ) ,
412+ "mid commit should be signed"
413+ ) ;
414+ assert ! (
415+ new_top_commit
416+ . decode( ) ?
417+ . extra_headers( )
418+ . pgp_signature( )
419+ . is_some( ) ,
420+ "top commit should be signed"
421+ ) ;
422+
423+ Ok ( ( ) )
424+ }
425+
426+ /// A commit should not be signed in the event it gets picked with a
427+ /// [`PickSignGuard::IfSignCommitsEnabled`]-guarded [`PickSignMode::IfChanged`], and Git-compatible
428+ /// signing is not enabled in the config.
429+ #[ test]
430+ fn commit_picked_with_ifchange_and_configuration_guard_is_not_signed_when_signing_config_is_not_enabled ( )
431+ -> Result < ( ) > {
432+ let ( repo, _tmpdir, mut meta) = fixture_writable_with_signing (
433+ "unsigned-commits-with-signing-key-setup-but-signing-disabled" ,
434+ ) ?;
435+
436+ let before = visualize_commit_graph_all ( & repo) ?;
437+ insta:: assert_snapshot!( before, @"
438+ * ea8caac (HEAD -> main, top) top
439+ * 135e6ba (mid) mid
440+ * 7a5aacf (base) base
441+ " ) ;
442+
443+ let graph = Graph :: from_head ( & repo, & * meta, standard_options ( ) ) ?. validated ( ) ?;
444+ let mut ws = graph. into_workspace ( ) ?;
445+
446+ let mut editor = Editor :: create_with_opts (
447+ & mut ws,
448+ & mut * meta,
449+ & repo,
450+ & GraphEditorOptions {
451+ sign_mode : PickSignMode :: IfChanged ( PickSignGuard :: IfSignCommitsEnabled ) ,
452+ } ,
453+ ) ?;
454+
455+ let top_commit_id = repo. rev_parse_single ( "top" ) ?. detach ( ) ;
456+ let mid_commit_id = repo. rev_parse_single ( "mid" ) ?. detach ( ) ;
457+
458+ // Delete the mid commit so the top commit gets picked. The top commit should _NOT_ get signed
459+ // as signing config is not enabled, and there is a sign guard in place on the pick.
460+ let mid_sel = editor. select_commit ( mid_commit_id) ?;
461+ editor. replace ( mid_sel, Step :: None ) ?;
462+
463+ let outcome = editor. rebase ( ) ?;
464+ let materialize_outcome = outcome. materialize ( ) ?;
465+
466+ let after = visualize_commit_graph_all ( & repo) ?;
467+ insta:: assert_snapshot!( after, @"
468+ * f923739 (HEAD -> main, top) top
469+ * 7a5aacf (mid, base) base
470+ " ) ;
471+
472+ let commit_mappings = materialize_outcome. history . commit_mappings ( ) ;
473+ assert_eq ! (
474+ commit_mappings. len( ) ,
475+ 1 ,
476+ "expected 1 commit to be cherry-picked"
477+ ) ;
478+ let new_top_commit_id = commit_mappings
479+ . get ( & top_commit_id)
480+ . expect ( "the head commit should be in the commit mappings" ) ;
481+
482+ let new_commit = repo. find_commit ( * new_top_commit_id) ?;
483+ assert ! (
484+ new_commit
485+ . decode( ) ?
486+ . extra_headers( )
487+ . pgp_signature( )
488+ . is_none( ) ,
489+ "the cherry-picked top commit should not be signed due to the sign guard"
490+ ) ;
491+
492+ Ok ( ( ) )
493+ }
494+
495+ /// Test for an edge case where a parent-less commit would not be signed even when picked with
496+ /// [`PickSignMode::Force`].
497+ #[ test]
498+ fn parentless_commit_picked_with_force_sign_is_signed ( ) -> Result < ( ) > {
499+ let ( repo, _tmpdir, mut meta) = fixture_writable_with_signing (
500+ "unsigned-commits-with-signing-key-setup-but-signing-disabled" ,
501+ ) ?;
502+
503+ let before = visualize_commit_graph_all ( & repo) ?;
504+ insta:: assert_snapshot!( before, @"
505+ * ea8caac (HEAD -> main, top) top
506+ * 135e6ba (mid) mid
507+ * 7a5aacf (base) base
508+ " ) ;
509+
510+ let graph = Graph :: from_head ( & repo, & * meta, standard_options ( ) ) ?. validated ( ) ?;
511+ let mut ws = graph. into_workspace ( ) ?;
512+
513+ let mut editor = Editor :: create_with_opts (
514+ & mut ws,
515+ & mut * meta,
516+ & repo,
517+ & GraphEditorOptions {
518+ sign_mode : PickSignMode :: IfChanged ( PickSignGuard :: IfSignCommitsEnabled ) ,
519+ } ,
520+ ) ?;
521+
522+ let base_commit_id = repo. rev_parse_single ( "base" ) ?. detach ( ) ;
523+
524+ // We pick the base commit with force, which should cause it to get signed.
525+ let base_sel = editor. select_commit ( base_commit_id) ?;
526+ let mut pick = Pick :: new_pick ( base_commit_id) ;
527+ pick. sign_mode = PickSignMode :: Force ;
528+ editor. replace ( base_sel, Step :: Pick ( pick) ) ?;
529+
530+ let outcome = editor. rebase ( ) ?;
531+ let materialize_outcome = outcome. materialize ( ) ?;
532+
533+ let commit_mappings = materialize_outcome. history . commit_mappings ( ) ;
534+ let new_base_commit_id = commit_mappings
535+ . get ( & base_commit_id)
536+ . expect ( "the base commit should be in the commit mappings" ) ;
537+
538+ let new_base_commit = repo. find_commit ( * new_base_commit_id) ?;
539+ assert ! (
540+ new_base_commit
541+ . decode( ) ?
542+ . extra_headers( )
543+ . pgp_signature( )
544+ . is_some( ) ,
545+ "the cherry-picked base commit should be signed"
546+ ) ;
547+
548+ Ok ( ( ) )
549+ }
0 commit comments