Skip to content

Commit c9024e9

Browse files
authored
Merge pull request #10410 from gitbutlerapp/gerrit-stuff
Basic gerrit support
2 parents 85aa1a0 + e9eba45 commit c9024e9

File tree

9 files changed

+187
-3
lines changed

9 files changed

+187
-3
lines changed

Cargo.lock

Lines changed: 24 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ but-api-macros = { path = "crates/but-api-macros" }
8989
but-claude = { path = "crates/but-claude" }
9090
but-cursor = { path = "crates/but-cursor" }
9191
but-broadcaster = { path = "crates/but-broadcaster" }
92+
but-gerrit = { path = "crates/but-gerrit" }
9293
git2-hooks = { version = "0.5.0" }
9394
itertools = "0.14.0"
9495
dirs = "6.0.0"

crates/but-core/src/settings.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ pub mod git {
99

1010
const GIT_SIGN_COMMITS: &str = "commit.gpgsign";
1111
const GITBUTLER_SIGN_COMMITS: &str = "gitbutler.signCommits";
12+
const GITBUTLER_GERRIT_MODE: &str = "gitbutler.gerritMode";
1213
const SIGNING_KEY: &str = "user.signingKey";
1314
const SIGNING_FORMAT: &str = "gpg.format";
1415
const GPG_PROGRAM: &str = "gpg.program";
@@ -25,6 +26,7 @@ pub mod git {
2526
pub struct GitConfigSettings {
2627
#[serde(rename = "signCommits")]
2728
pub gitbutler_sign_commits: Option<bool>,
29+
pub gitbutler_gerrit_mode: Option<bool>,
2830
pub signing_key: Option<BStringForFrontend>,
2931
pub signing_format: Option<BStringForFrontend>,
3032
pub gpg_program: Option<BStringForFrontend>,
@@ -35,6 +37,7 @@ pub mod git {
3537
fn from(
3638
crate::GitConfigSettings {
3739
gitbutler_sign_commits,
40+
gitbutler_gerrit_mode,
3841
signing_key,
3942
signing_format,
4043
gpg_program,
@@ -43,6 +46,7 @@ pub mod git {
4346
) -> Self {
4447
GitConfigSettings {
4548
gitbutler_sign_commits,
49+
gitbutler_gerrit_mode,
4650
signing_key: signing_key.map(Into::into),
4751
signing_format: signing_format.map(Into::into),
4852
gpg_program: gpg_program
@@ -57,6 +61,7 @@ pub mod git {
5761
fn from(
5862
GitConfigSettings {
5963
gitbutler_sign_commits,
64+
gitbutler_gerrit_mode,
6065
signing_key,
6166
signing_format,
6267
gpg_program,
@@ -65,6 +70,7 @@ pub mod git {
6570
) -> Self {
6671
crate::GitConfigSettings {
6772
gitbutler_sign_commits,
73+
gitbutler_gerrit_mode,
6874
signing_key: signing_key.map(Into::into),
6975
signing_format: signing_format.map(Into::into),
7076
gpg_program: gpg_program.map(Into::into),
@@ -89,6 +95,8 @@ pub mod git {
8995
/// * `commit.gpgsign` which is otherwise valid.
9096
/// * otherwise it defaults to `false` just like Git would.
9197
pub gitbutler_sign_commits: Option<bool>,
98+
/// If `true`, GitButler will create ChangeId trailers and will push references in the Gerrit way
99+
pub gitbutler_gerrit_mode: Option<bool>,
92100
/// `user.signingKey`.
93101
pub signing_key: Option<BString>,
94102
/// `gpg.format`
@@ -108,12 +116,14 @@ pub mod git {
108116
.boolean(GITBUTLER_SIGN_COMMITS)
109117
.or_else(|| config.boolean(GIT_SIGN_COMMITS))
110118
.or(Some(false));
119+
let gitbutler_gerrit_mode = config.boolean(GITBUTLER_GERRIT_MODE).or(Some(false));
111120
let signing_key = config.string(SIGNING_KEY).map(Cow::into_owned);
112121
let signing_format = config.string(SIGNING_FORMAT).map(Cow::into_owned);
113122
let gpg_program = config.trusted_program(GPG_PROGRAM).map(Cow::into_owned);
114123
let gpg_ssh_program = config.trusted_program(GPG_SSH_PROGRAM).map(Cow::into_owned);
115124
Ok(GitConfigSettings {
116125
gitbutler_sign_commits,
126+
gitbutler_gerrit_mode,
117127
signing_key,
118128
signing_format,
119129
gpg_program,

crates/but-core/tests/core/settings.rs

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,16 @@ mod git {
1212
actual,
1313
GitConfigSettings {
1414
gitbutler_sign_commits: Some(false),
15-
..GitConfigSettings::default()
15+
..GitConfigSettings {
16+
gitbutler_gerrit_mode: Some(false),
17+
..Default::default()
18+
}
1619
},
1720
"by default, None of these are set in a new repository, except for the explicit gpg-sign logic"
1821
);
1922
let expected = GitConfigSettings {
2023
gitbutler_sign_commits: Some(true),
24+
gitbutler_gerrit_mode: Some(false),
2125
signing_key: Some("signing key".into()),
2226
signing_format: Some("signing format".into()),
2327
gpg_program: Some("gpg program".into()),
@@ -40,7 +44,10 @@ mod git {
4044
let repo = gix::open_opts(tmp.path(), gix::open::Options::isolated())?;
4145
let expected = GitConfigSettings {
4246
gitbutler_sign_commits: Some(true),
43-
..Default::default()
47+
..GitConfigSettings {
48+
gitbutler_gerrit_mode: Some(false),
49+
..Default::default()
50+
}
4451
};
4552

4653
repo.set_git_settings(&expected)?;

crates/but-gerrit/Cargo.toml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
[package]
2+
name = "but-gerrit"
3+
version = "0.0.0"
4+
edition = "2024"
5+
authors = ["GitButler <[email protected]>"]
6+
publish = false
7+
8+
[lib]
9+
doctest = false
10+
test = false
11+
12+
[dependencies]
13+
sha-1 = "0.10.1"
14+
uuid.workspace = true
15+
anyhow.workspace = true
16+
bstr.workspace = true
17+
but-core.workspace = true
18+
gix = { workspace = true }

crates/but-gerrit/src/lib.rs

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
use bstr::{BString, ByteSlice};
2+
use but_core::commit::HeadersV2;
3+
use sha1::{Digest, Sha1};
4+
use std::fmt::Display;
5+
use uuid::Uuid;
6+
7+
#[derive(Clone, Debug)]
8+
pub struct GerritChangeId(String);
9+
10+
impl From<Uuid> for GerritChangeId {
11+
fn from(value: Uuid) -> Self {
12+
let mut hasher = Sha1::new();
13+
hasher.update(value);
14+
Self(format!("I{:x}", hasher.finalize()))
15+
}
16+
}
17+
impl Display for GerritChangeId {
18+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
19+
write!(f, "{}", self.0)
20+
}
21+
}
22+
23+
pub fn set_trailers(commit: &mut gix::objs::Commit) {
24+
if let Some(headers) = HeadersV2::try_from_commit(commit) {
25+
commit.message = with_change_id_trailer(commit.message.clone(), headers.change_id.into());
26+
}
27+
}
28+
29+
fn with_change_id_trailer(msg: BString, change_id: Uuid) -> BString {
30+
let change_id = GerritChangeId::from(change_id);
31+
let change_id_line = format!("\nChange-Id: {change_id}\n");
32+
let msg_bytes = msg.as_slice();
33+
34+
if msg_bytes.find(b"\nChange-Id:").is_some() {
35+
return msg;
36+
}
37+
38+
let lines: Vec<&[u8]> = msg_bytes.lines().collect();
39+
let mut insert_pos = lines.len();
40+
41+
for (i, line) in lines.iter().enumerate().rev() {
42+
if line.starts_with(b"Signed-off-by:") {
43+
insert_pos = i;
44+
}
45+
}
46+
47+
let mut result = BString::from(Vec::new());
48+
for (i, line) in lines.iter().enumerate() {
49+
if i == insert_pos {
50+
result.extend_from_slice(change_id_line.as_bytes());
51+
}
52+
result.extend_from_slice(line);
53+
result.push(b'\n');
54+
}
55+
56+
if insert_pos == lines.len() {
57+
result.extend_from_slice(change_id_line.as_bytes());
58+
}
59+
60+
result
61+
}
62+
63+
#[cfg(test)]
64+
mod tests {
65+
use super::*;
66+
#[test]
67+
fn output_is_41_characters_long() {
68+
let uuid = Uuid::new_v4();
69+
let change_id = GerritChangeId::from(uuid);
70+
let output = format!("{change_id}");
71+
assert_eq!(output.len(), 41); // "I" + 40 hex chars
72+
assert!(output.starts_with('I'));
73+
}
74+
75+
#[test]
76+
fn test_add_trailers() {
77+
let uuid = Uuid::new_v4();
78+
let change_id = GerritChangeId::from(uuid);
79+
let change_id_line = format!("Change-Id: {change_id}\n");
80+
81+
// Case 1: No trailers
82+
let msg = BString::from("Initial commit\n");
83+
let updated_msg = with_change_id_trailer(msg.clone(), uuid);
84+
assert!(
85+
updated_msg
86+
.as_slice()
87+
.windows(change_id_line.len())
88+
.any(|w| w == change_id_line.as_bytes())
89+
);
90+
91+
// Case 2: Already has Change-Id
92+
let msg_with_change_id = BString::from(format!("Initial commit\n{change_id_line}"));
93+
let updated_msg = with_change_id_trailer(msg_with_change_id.clone(), uuid);
94+
assert_eq!(updated_msg, msg_with_change_id);
95+
96+
// Case 3: Has Signed-off-by trailer
97+
let msg_with_signed_off =
98+
BString::from("Initial commit\nSigned-off-by: User <[email protected]>\n");
99+
let updated_msg = with_change_id_trailer(msg_with_signed_off.clone(), uuid);
100+
let updated_msg_str = updated_msg.as_bstr();
101+
let change_id_index = updated_msg_str.find(&change_id_line).unwrap();
102+
let signed_off_index = updated_msg_str.find("Signed-off-by:").unwrap();
103+
assert!(change_id_index < signed_off_index);
104+
}
105+
}

crates/but-rebase/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ bstr.workspace = true
1919
tempfile.workspace = true
2020
serde = { version = "1.0.217", features = ["derive"] }
2121
toml.workspace = true
22+
but-gerrit.workspace = true
2223

2324
[dev-dependencies]
2425
but-testsupport.workspace = true

crates/but-rebase/src/commit.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,9 @@ pub fn create(
110110
{
111111
commit.extra_headers.remove(pos);
112112
}
113+
if repo.git_settings()?.gitbutler_gerrit_mode.unwrap_or(false) {
114+
but_gerrit::set_trailers(&mut commit);
115+
}
113116
if repo.git_settings()?.gitbutler_sign_commits.unwrap_or(false) {
114117
let mut buf = Vec::new();
115118
commit.write_to(&mut buf)?;

crates/gitbutler-branch-actions/src/stack.rs

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use anyhow::{Context, Result};
2+
use but_core::RepositoryExt;
23
use gitbutler_command_context::CommandContext;
34
use gitbutler_oplog::entry::{OperationKind, SnapshotDetails};
45
use gitbutler_oplog::{OplogExt, SnapshotExt};
@@ -189,6 +190,10 @@ pub fn push_stack(
189190
remote: default_target.push_remote_name(),
190191
branch_to_remote: vec![],
191192
};
193+
let gerrit_mode = gix_repo
194+
.git_settings()?
195+
.gitbutler_gerrit_mode
196+
.unwrap_or(false);
192197

193198
let force_push_protection = !skip_force_push_protection && ctx.project().force_push_protection;
194199

@@ -230,12 +235,22 @@ pub fn push_stack(
230235
}
231236
}
232237

238+
let refspec = if gerrit_mode {
239+
Some(format!(
240+
"{}:refs/for/{}",
241+
push_details.head,
242+
default_target.branch.branch()
243+
))
244+
} else {
245+
None
246+
};
247+
233248
ctx.push(
234249
push_details.head,
235250
&push_details.remote_refname,
236251
with_force,
237252
force_push_protection,
238-
None,
253+
refspec,
239254
Some(Some(stack.id)),
240255
)?;
241256

0 commit comments

Comments
 (0)