Skip to content

Commit 647a01c

Browse files
committed
test: add tests for the new behavior in the rebase engine
1 parent a9a56d7 commit 647a01c

2 files changed

Lines changed: 386 additions & 1 deletion

File tree

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
#!/bin/bash
2+
#
3+
# Description: Scenario with unsigned commits and commit signing disabled, but
4+
# all the requisite configuration to sign commits is present.
5+
#
6+
# The scenario is designed to be used to test signing of commits in the absence
7+
# of other changes.
8+
9+
set -eu -o pipefail
10+
11+
git init
12+
13+
ssh-keygen -t rsa -b 2048 -C "test@example.com" -N "" -f signature.key
14+
git config gpg.format ssh
15+
git config user.signingKey "$PWD/signature.key"
16+
echo "*.key*" >.gitignore
17+
18+
echo "base" >base && git add . && git commit -m "base"
19+
git update-ref refs/heads/base $(git rev-parse HEAD)
20+
echo "mid" >mid && git add . && git commit -m "mid"
21+
git update-ref refs/heads/mid $(git rev-parse HEAD)
22+
echo "top" >top && git add . && git commit -m "top"
23+
git update-ref refs/heads/top $(git rev-parse HEAD)

crates/but-rebase/tests/rebase/graph_rebase/signing_preferences.rs

Lines changed: 363 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@ use std::fs;
33
/// These tests cover the `sign_mode` property on the Step::Pick.
44
use anyhow::Result;
55
use 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+
};
710
use but_testsupport::{cat_commit, visualize_commit_graph_all};
811

912
use 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

Comments
 (0)