Skip to content

Commit b09ba4e

Browse files
committed
Implement adding of Gerrit trailers
1 parent 85aa1a0 commit b09ba4e

File tree

6 files changed

+150
-0
lines changed

6 files changed

+150
-0
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-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: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@ pub fn create(
110110
{
111111
commit.extra_headers.remove(pos);
112112
}
113+
but_gerrit::set_trailers(&mut commit);
113114
if repo.git_settings()?.gitbutler_sign_commits.unwrap_or(false) {
114115
let mut buf = Vec::new();
115116
commit.write_to(&mut buf)?;

0 commit comments

Comments
 (0)