Skip to content

Commit 906588d

Browse files
authored
Merge pull request #13046 from gitbutlerapp/GB-1121/but-rebase-sign-on-push
feat(but-rebase): add programmatic signing control for graph engine
2 parents 7ff83f7 + ae2ce0d commit 906588d

14 files changed

Lines changed: 684 additions & 84 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: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
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

@@ -10,13 +10,38 @@ use crate::graph_rebase::{
1010
SuccessfulRebase, util,
1111
};
1212

13+
#[derive(Clone, Copy)]
14+
/// Options for the editor.
15+
pub struct GraphEditorOptions {
16+
/// Determines how cherry-picked commits are signed.
17+
pub default_sign_commit: SignCommit,
18+
}
19+
20+
impl Default for GraphEditorOptions {
21+
fn default() -> Self {
22+
Self {
23+
default_sign_commit: SignCommit::IfSignCommitsEnabled,
24+
}
25+
}
26+
}
27+
1328
/// Creates an editor out of the workspace graph.
1429
impl<'ws, 'meta, M: RefMetadata> Editor<'ws, 'meta, M> {
15-
/// Creates an editor out of the workspace graph.
30+
/// Creates an editor out of the workspace graph with the default options.
1631
pub fn create(
1732
workspace: &'ws mut but_graph::projection::Workspace,
1833
meta: &'meta mut M,
1934
repo: &gix::Repository,
35+
) -> Result<Self> {
36+
Self::create_with_opts(workspace, meta, repo, &GraphEditorOptions::default())
37+
}
38+
39+
/// Creates an editor out of the workspace graph with the specified options.
40+
pub fn create_with_opts(
41+
workspace: &'ws mut but_graph::projection::Workspace,
42+
meta: &'meta mut M,
43+
repo: &gix::Repository,
44+
options: &GraphEditorOptions,
2045
) -> Result<Self> {
2146
// This first creates runs of nodes and associates them with the
2247
// but-graph segments. We then do a second pass over all the segments
@@ -91,7 +116,9 @@ impl<'ws, 'meta, M: RefMetadata> Editor<'ws, 'meta, M> {
91116
let pick = if workspace_commit_id == Some(commit.id) {
92117
Pick::new_workspace_pick(commit.id)
93118
} else {
94-
Pick::new_pick(commit.id)
119+
let mut pick = Pick::new_pick(commit.id);
120+
pick.sign_commit = options.default_sign_commit;
121+
pick
95122
};
96123
let ix = graph.add_node(Step::Pick(pick));
97124
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)