Skip to content

Commit a7f787c

Browse files
committed
feat: enable programmatic signing in graph rebase engine
This commit puts down the groundwork for enabling sign-on-push functionality by introducing new facilities to tweak sign behavior for the graph rebase engine. The key design goal is to allow for programmatically driven signing, where we can decide at runtime if a certain rebase operation should cause signing (e.g. just before a push), while another one should not. At the same time, we want to retain the old gitbutler.signCommits behavior for the sake of Git interoperability - you might do some work with GitButler and then decide to push with Git, and that should still work, respecting signing settings. The two concepts introduced to the graph rebase engine in this commit is a `PickMode`, allowing the caller to tune when a commit is cherry-picked, and `SignCommit`, which allows for tuning signing behavior beyond what was previously possible. Setting `PickMode::Force` and `SignCommit::Yes` or `SignCommit::IfSignCommitsEnabled` allows one to sign a commit that is otherwise unchanged. For descendants to also be signed, they need to be picked with an appropriate `SignCommit` setting. None of the new behavior is as of yet exposed in any of the GitButler UIs. That will come later down the road with sign-on-push functionality.
1 parent 4d8fc18 commit a7f787c

13 files changed

Lines changed: 302 additions & 85 deletions

File tree

crates/but-rebase/src/cherry_pick.rs

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ pub(crate) mod function {
2727

2828
use anyhow::{Context as _, bail};
2929
use bstr::BString;
30-
use but_core::commit::{HEADERS_CONFLICTED_FIELD, Headers, TreeKind};
30+
use but_core::commit::{HEADERS_CONFLICTED_FIELD, Headers, SignCommit, TreeKind};
3131
use gix::{object::tree::EntryKind, prelude::ObjectIdExt};
3232
use serde::Serialize;
3333

@@ -188,10 +188,13 @@ pub(crate) mod function {
188188
)));
189189
}
190190
set_parent(&mut new_commit, head.id.detach())?;
191-
Ok(
192-
crate::commit::create(repo, new_commit, DateMode::CommitterUpdateAuthorKeep, true)?
193-
.attach(repo),
194-
)
191+
Ok(crate::commit::create(
192+
repo,
193+
new_commit,
194+
DateMode::CommitterUpdateAuthorKeep,
195+
SignCommit::IfSignCommitsEnabled,
196+
)?
197+
.attach(repo))
195198
}
196199

197200
fn commit_from_conflicted_tree<'repo>(
@@ -254,7 +257,7 @@ pub(crate) mod function {
254257
repo,
255258
to_rebase.inner,
256259
DateMode::CommitterUpdateAuthorKeep,
257-
true,
260+
SignCommit::IfSignCommitsEnabled,
258261
)?
259262
.attach(repo))
260263
}

crates/but-rebase/src/commit.rs

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ pub fn create(
8686
repo: &gix::Repository,
8787
mut commit: gix::objs::Commit,
8888
committer: DateMode,
89-
sign_if_configured: bool,
89+
sign_commit: SignCommit,
9090
) -> anyhow::Result<gix::ObjectId> {
9191
match committer {
9292
DateMode::CommitterUpdateAuthorKeep => {
@@ -102,16 +102,7 @@ pub fn create(
102102
if settings.gitbutler_gerrit_mode.unwrap_or(false) {
103103
but_gerrit::set_trailers(&mut commit);
104104
}
105-
but_core::commit::create(
106-
repo,
107-
commit,
108-
None,
109-
if sign_if_configured {
110-
SignCommit::IfSignCommitsEnabled
111-
} else {
112-
SignCommit::No
113-
},
114-
)
105+
but_core::commit::create(repo, commit, None, sign_commit)
115106
}
116107

117108
/// Update the committer of `commit` to be the current one.

crates/but-rebase/src/graph_rebase/cherry_pick.rs

Lines changed: 39 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ use anyhow::{Context as _, Result, bail};
66
use bstr::BString;
77
use but_core::{
88
RepositoryExt as _,
9-
commit::{HEADERS_CONFLICTED_FIELD, Headers, TreeKind},
9+
commit::{HEADERS_CONFLICTED_FIELD, Headers, SignCommit, TreeKind},
1010
};
1111
use gix::{objs::tree::EntryKind, prelude::ObjectIdExt as _};
1212

@@ -35,7 +35,18 @@ pub enum CherryPickOutcome {
3535
},
3636
}
3737

38-
/// Cherry pick, but supports supports cherry-picking merge commits.
38+
/// Controls when a commit is cherry-picked during a rebase.
39+
#[derive(Debug, Clone, Copy, PartialEq)]
40+
pub enum PickMode {
41+
/// Cherry-picks the commit only if it's necessary because of changes to the commit or its
42+
/// parents.
43+
IfChanged,
44+
/// Forces a cherry-pick on a commit. This is for example useful in combination with
45+
/// [`SignCommit`] to sign/unsign commits that are otherwise unchanged.
46+
Force,
47+
}
48+
49+
/// Cherry pick, but supports cherry-picking merge commits.
3950
///
4051
/// When cherry-picking a commit onto two or more commits, we first find the
4152
/// merge of the two "onto" commits, and then cherry-pick onto that.
@@ -52,15 +63,19 @@ pub enum CherryPickOutcome {
5263
///
5364
/// Except in the case where X is conflicted. In that case we then make use of
5465
/// X's "base" sub-tree as the base.
66+
///
67+
/// `pick_mode` - controls how to determine if a commit should be cherry-picked.
68+
/// `sign_commit` - controls how the resulting commit is signed.
5569
pub fn cherry_pick(
5670
repo: &gix::Repository,
5771
target: gix::ObjectId,
5872
ontos: &[gix::ObjectId],
59-
sign_if_configured: bool,
73+
pick_mode: PickMode,
74+
sign_commit: SignCommit,
6075
) -> Result<CherryPickOutcome> {
6176
let target = but_core::Commit::from_id(target.attach(repo))?;
6277

63-
if ontos == target.parents.as_slice() {
78+
if ontos == target.parents.as_slice() && pick_mode != PickMode::Force {
6479
// We don't need to rebase
6580
return Ok(CherryPickOutcome::Identity(target.id.detach()));
6681
}
@@ -72,9 +87,22 @@ pub fn cherry_pick(
7287
let onto_t = tree_from_merging_commits(repo, ontos, TreeKind::AutoResolution)?;
7388

7489
match (&base_t, &onto_t) {
90+
(MergeOutcome::NoCommit, MergeOutcome::NoCommit) if pick_mode == PickMode::Force => {
91+
// We should only end up here when trying to force-pick a parentless commit. At that
92+
// point, it's safe to simply recreate that commit outright.
93+
//
94+
// Currently, the only known use case for this is to forcibly sign/unsign root commits.
95+
let commit = crate::commit::create(
96+
repo,
97+
target.inner,
98+
DateMode::CommitterUpdateAuthorKeep,
99+
sign_commit,
100+
)?;
101+
Ok(CherryPickOutcome::Commit(commit))
102+
}
75103
(MergeOutcome::NoCommit, MergeOutcome::NoCommit) => {
76104
// We shouldn't actually ever hit this because it should be handled
77-
// by the ontos & parents comparison.
105+
// by the ontos & parents comparison or the PickMode::Force case.
78106
Ok(CherryPickOutcome::Identity(target.id.detach()))
79107
}
80108
// TODO(cto): We can handle the specific case where (the base is
@@ -130,15 +158,14 @@ pub fn cherry_pick(
130158
base_t,
131159
onto_t,
132160
target_t.detach(),
133-
sign_if_configured,
161+
sign_commit,
134162
)?;
135163
Ok(CherryPickOutcome::ConflictedCommit(
136164
conflicted_commit.detach(),
137165
))
138166
} else {
139167
Ok(CherryPickOutcome::Commit(
140-
commit_from_unconflicted_tree(ontos, target, tree_id, sign_if_configured)?
141-
.detach(),
168+
commit_from_unconflicted_tree(ontos, target, tree_id, sign_commit)?.detach(),
142169
))
143170
}
144171
}
@@ -273,7 +300,7 @@ fn commit_from_unconflicted_tree<'repo>(
273300
parents: &[gix::ObjectId],
274301
to_rebase: but_core::Commit<'repo>,
275302
resolved_tree_id: gix::Id<'repo>,
276-
sign_if_configured: bool,
303+
sign_commit: SignCommit,
277304
) -> anyhow::Result<gix::Id<'repo>> {
278305
let repo = to_rebase.id.repo;
279306

@@ -303,7 +330,7 @@ fn commit_from_unconflicted_tree<'repo>(
303330
repo,
304331
new_commit,
305332
DateMode::CommitterUpdateAuthorKeep,
306-
sign_if_configured,
333+
sign_commit,
307334
)?
308335
.attach(repo))
309336
}
@@ -318,7 +345,7 @@ fn commit_from_conflicted_tree<'repo>(
318345
base_tree_id: gix::ObjectId,
319346
ours_tree_id: gix::ObjectId,
320347
theirs_tree_id: gix::ObjectId,
321-
sign_if_configured: bool,
348+
sign_commit: SignCommit,
322349
) -> anyhow::Result<gix::Id<'repo>> {
323350
let repo = resolved_tree_id.repo;
324351
// in case someone checks this out with vanilla Git, we should warn why it looks like this
@@ -370,7 +397,7 @@ fn commit_from_conflicted_tree<'repo>(
370397
repo,
371398
to_rebase.inner,
372399
DateMode::CommitterUpdateAuthorKeep,
373-
sign_if_configured,
400+
sign_commit,
374401
)?
375402
.attach(repo))
376403
}

crates/but-rebase/src/graph_rebase/commit.rs

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
33
use anyhow::{Context, Result, bail};
44
use but_core::RefMetadata;
5+
use but_core::commit::SignCommit;
56
use gix::prelude::ObjectIdExt;
67

78
use crate::{
@@ -74,9 +75,12 @@ impl<M: RefMetadata> Editor<'_, '_, M> {
7475
commit: but_core::CommitOwned,
7576
date_mode: DateMode,
7677
) -> Result<gix::ObjectId> {
77-
// TODO(GB-983): As part of moving to only signing at the materializing
78-
// step, this should have sign_if_configured false here.
79-
create(&self.repo, commit.inner, date_mode, true)
78+
create(
79+
&self.repo,
80+
commit.inner,
81+
date_mode,
82+
SignCommit::IfSignCommitsEnabled,
83+
)
8084
}
8185

8286
/// Writes a commit with correct signing to the in memory repository.

crates/but-rebase/src/graph_rebase/creation.rs

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,50 @@
11
use std::collections::{HashMap, HashSet};
22

33
use anyhow::{Result, bail};
4-
use but_core::RefMetadata;
4+
use but_core::{RefMetadata, commit::SignCommit};
55
use but_graph::{Commit, SegmentIndex};
66
use petgraph::{Direction, visit::EdgeRef as _};
77

88
use crate::graph_rebase::{
99
Checkout, Edge, Editor, Pick, RevisionHistory, Selector, Step, StepGraph, StepGraphIndex,
10-
SuccessfulRebase, util,
10+
SuccessfulRebase, cherry_pick::PickMode, util,
1111
};
1212

13+
#[derive(Clone, Copy)]
14+
/// Options for the editor.
15+
pub struct GraphEditorOptions {
16+
/// Determines when picks are cherry-picked during a rebase.
17+
pub default_pick_mode: PickMode,
18+
/// Determines how resulting commits are signed.
19+
pub default_sign_commit: SignCommit,
20+
}
21+
22+
impl Default for GraphEditorOptions {
23+
fn default() -> Self {
24+
Self {
25+
default_pick_mode: PickMode::IfChanged,
26+
default_sign_commit: SignCommit::IfSignCommitsEnabled,
27+
}
28+
}
29+
}
30+
1331
/// Creates an editor out of the workspace graph.
1432
impl<'ws, 'meta, M: RefMetadata> Editor<'ws, 'meta, M> {
15-
/// Creates an editor out of the workspace graph.
33+
/// Creates an editor out of the workspace graph with the default options.
1634
pub fn create(
1735
workspace: &'ws mut but_graph::projection::Workspace,
1836
meta: &'meta mut M,
1937
repo: &gix::Repository,
38+
) -> Result<Self> {
39+
Self::create_with_opts(workspace, meta, repo, &GraphEditorOptions::default())
40+
}
41+
42+
/// Creates an editor out of the workspace graph with the specified options.
43+
pub fn create_with_opts(
44+
workspace: &'ws mut but_graph::projection::Workspace,
45+
meta: &'meta mut M,
46+
repo: &gix::Repository,
47+
options: &GraphEditorOptions,
2048
) -> Result<Self> {
2149
// This first creates runs of nodes and associates them with the
2250
// but-graph segments. We then do a second pass over all the segments
@@ -91,7 +119,10 @@ impl<'ws, 'meta, M: RefMetadata> Editor<'ws, 'meta, M> {
91119
let pick = if workspace_commit_id == Some(commit.id) {
92120
Pick::new_workspace_pick(commit.id)
93121
} else {
94-
Pick::new_pick(commit.id)
122+
let mut pick = Pick::new_pick(commit.id);
123+
pick.pick_mode = options.default_pick_mode;
124+
pick.sign_commit = options.default_sign_commit;
125+
pick
95126
};
96127
let ix = graph.add_node(Step::Pick(pick));
97128
commit_to_pick_ix.insert(commit.id, ix);

crates/but-rebase/src/graph_rebase/mod.rs

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,14 @@ pub mod rebase;
99
use std::collections::{BTreeMap, HashMap};
1010

1111
use anyhow::{Context, Result, bail};
12-
use but_core::RefMetadata;
12+
use but_core::{RefMetadata, commit::SignCommit};
1313
use but_graph::init::Overlay;
14+
pub use creation::GraphEditorOptions;
1415
use gix::refs::transaction::RefEdit;
1516

1617
use crate::graph_rebase::util::collect_ordered_parents;
18+
19+
use crate::graph_rebase::cherry_pick::PickMode;
1720
pub mod cherry_pick;
1821
pub mod commit;
1922
pub mod materialize;
@@ -41,9 +44,14 @@ pub struct Pick {
4144
/// If set to true, a rebase will fail if not all of the parents (outgoing
4245
/// nodes) are references.
4346
pub parents_must_be_references: bool,
44-
/// If set to true, the rebase engine will try to sign the commit if it
45-
/// gets cherry-picked and the user has configured signing.
46-
pub sign_if_configured: bool,
47+
/// Controls under what circumstances the commit is cherry-picked.
48+
pub pick_mode: PickMode,
49+
/// Controls whether the resulting commit is signed.
50+
///
51+
/// Note that signing a parent commit only causes descendants to be signed if those descendants
52+
/// are also picked with a `sign_commit` value that enables signing (e.g. [`SignCommit::Yes`]
53+
/// or [`SignCommit::IfSignCommitsEnabled`] with config enabled).
54+
pub sign_commit: SignCommit,
4755
/// Exclude the commit from being included in the
4856
/// [`RevisionHistory::commit_mappings()`]. This is helpful if we are
4957
/// creating a new commit since the the mappings will be non-sensical to the
@@ -59,7 +67,8 @@ impl Pick {
5967
preserved_parents: None,
6068
conflictable: true,
6169
parents_must_be_references: false,
62-
sign_if_configured: true,
70+
pick_mode: PickMode::IfChanged,
71+
sign_commit: SignCommit::IfSignCommitsEnabled,
6372
exclude_from_tracking: false,
6473
}
6574
}
@@ -81,7 +90,8 @@ impl Pick {
8190
preserved_parents: None,
8291
conflictable: false,
8392
parents_must_be_references: true,
84-
sign_if_configured: false,
93+
pick_mode: PickMode::IfChanged,
94+
sign_commit: SignCommit::No,
8595
exclude_from_tracking: false,
8696
}
8797
}
@@ -419,7 +429,9 @@ impl RevisionHistory {
419429
mod test {
420430
use std::str::FromStr;
421431

422-
use crate::graph_rebase::Pick;
432+
use but_core::commit::SignCommit;
433+
434+
use crate::graph_rebase::{Pick, cherry_pick::PickMode};
423435

424436
#[test]
425437
fn workspace_commit_defaults() -> anyhow::Result<()> {
@@ -432,7 +444,8 @@ mod test {
432444
preserved_parents: None,
433445
conflictable: false,
434446
parents_must_be_references: true,
435-
sign_if_configured: false,
447+
pick_mode: PickMode::IfChanged,
448+
sign_commit: SignCommit::No,
436449
exclude_from_tracking: false
437450
}
438451
);
@@ -451,7 +464,8 @@ mod test {
451464
preserved_parents: None,
452465
conflictable: true,
453466
parents_must_be_references: false,
454-
sign_if_configured: true,
467+
pick_mode: PickMode::IfChanged,
468+
sign_commit: SignCommit::IfSignCommitsEnabled,
455469
exclude_from_tracking: false
456470
}
457471
);

crates/but-rebase/src/graph_rebase/rebase.rs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,8 +70,13 @@ impl<'ws, 'graph, M: RefMetadata> Editor<'ws, 'graph, M> {
7070
.collect::<Result<Vec<_>>>()?,
7171
};
7272

73-
let outcome =
74-
cherry_pick(&self.repo, pick.id, &ontos, pick.sign_if_configured)?;
73+
let outcome = cherry_pick(
74+
&self.repo,
75+
pick.id,
76+
&ontos,
77+
pick.pick_mode,
78+
pick.sign_commit,
79+
)?;
7580

7681
if matches!(outcome, CherryPickOutcome::ConflictedCommit(_))
7782
&& !pick.conflictable

0 commit comments

Comments
 (0)