Skip to content

Commit a5ff607

Browse files
committed
Change handling of headers
1 parent 24d67b4 commit a5ff607

File tree

12 files changed

+241
-100
lines changed

12 files changed

+241
-100
lines changed

crates/gitbutler-core/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ edition = "2021"
55
authors = ["GitButler <gitbutler@gitbutler.com>"]
66
publish = false
77

8+
89
[dev-dependencies]
910
once_cell = "1.19"
1011
pretty_assertions = "1.4"
Lines changed: 40 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,89 +1,91 @@
1-
use anyhow::Result;
1+
use bstr::{BStr, BString, ByteSlice, ByteVec};
22
use core::str;
33

4+
use super::CommitHeadersV2;
5+
46
pub struct CommitBuffer {
5-
heading: Vec<(String, String)>,
6-
message: String,
7+
heading: Vec<(BString, BString)>,
8+
message: BString,
79
}
810

911
impl CommitBuffer {
10-
pub fn new(buffer: &[u8]) -> Result<Self> {
11-
let buffer = str::from_utf8(buffer)?;
12-
13-
if let Some((heading, message)) = buffer.split_once("\n\n") {
12+
pub fn new(buffer: &[u8]) -> Self {
13+
let buffer = BStr::new(buffer);
14+
if let Some((heading, message)) = buffer.split_once_str("\n\n") {
1415
let heading = heading
1516
.lines()
16-
.filter_map(|line| line.split_once(' '))
17-
.map(|(key, value)| (key.to_string(), value.to_string()))
17+
.filter_map(|line| line.split_once_str(" "))
18+
.map(|(key, value)| (key.into(), value.into()))
1819
.collect();
1920

20-
Ok(Self {
21+
Self {
2122
heading,
22-
message: message.to_string(),
23-
})
23+
message: message.into(),
24+
}
2425
} else {
25-
Ok(Self {
26+
Self {
2627
heading: vec![],
27-
message: buffer.to_string(),
28-
})
28+
message: buffer.into(),
29+
}
2930
}
3031
}
3132

3233
pub fn set_header(&mut self, key: &str, value: &str) {
3334
let mut set_heading = false;
3435
self.heading.iter_mut().for_each(|(k, v)| {
3536
if k == key {
36-
*v = value.to_string();
37+
*v = value.into();
3738
set_heading = true;
3839
}
3940
});
4041

4142
if !set_heading {
42-
self.heading.push((key.to_string(), value.to_string()));
43+
self.heading.push((key.into(), value.into()));
4344
}
4445
}
4546

46-
pub fn set_change_id(&mut self, change_id: Option<&str>) {
47-
let change_id = change_id
48-
.map(|id| id.to_string())
49-
.unwrap_or_else(|| format!("{}", uuid::Uuid::new_v4()));
50-
51-
self.set_header("change-id", change_id.as_str());
47+
/// Defers to the CommitHeadersV2 struct about which headers should be injected.
48+
/// If `commit_headers: None` is provided, a default set of headers, including a generated change-id will be used
49+
pub fn set_gitbutler_headers(&mut self, commit_headers: Option<CommitHeadersV2>) {
50+
if let Some(commit_headers) = commit_headers {
51+
commit_headers.inject_into(self)
52+
} else {
53+
CommitHeadersV2::inject_default(self)
54+
}
5255
}
5356

54-
pub fn as_string(&self) -> String {
55-
let mut output = String::new();
57+
pub fn as_bstring(&self) -> BString {
58+
let mut output = BString::new(vec![]);
5659

5760
for (key, value) in &self.heading {
58-
output.push_str(&format!("{} {}\n", key, value));
61+
output.push_str(key);
62+
output.push_str(" ");
63+
output.push_str(value);
64+
output.push_str("\n");
5965
}
6066

61-
output.push('\n');
67+
output.push_str("\n");
6268

6369
output.push_str(&self.message);
6470

6571
output
6672
}
6773
}
6874

69-
impl TryFrom<git2::Buf> for CommitBuffer {
70-
type Error = anyhow::Error;
71-
72-
fn try_from(git2_buffer: git2::Buf) -> Result<Self> {
75+
impl From<git2::Buf> for CommitBuffer {
76+
fn from(git2_buffer: git2::Buf) -> Self {
7377
Self::new(&git2_buffer)
7478
}
7579
}
7680

77-
impl TryFrom<String> for CommitBuffer {
78-
type Error = anyhow::Error;
79-
80-
fn try_from(s: String) -> Result<Self> {
81+
impl From<BString> for CommitBuffer {
82+
fn from(s: BString) -> Self {
8183
Self::new(s.as_bytes())
8284
}
8385
}
8486

85-
impl From<CommitBuffer> for String {
86-
fn from(buffer: CommitBuffer) -> String {
87-
buffer.as_string()
87+
impl From<CommitBuffer> for BString {
88+
fn from(buffer: CommitBuffer) -> BString {
89+
buffer.as_bstring()
8890
}
8991
}

crates/gitbutler-core/src/git/commit_ext.rs

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
// use anyhow::Result;
21
use bstr::BStr;
32

3+
use super::HasCommitHeaders;
4+
45
/// Extension trait for `git2::Commit`.
56
///
67
/// For now, it collects useful methods from `gitbutler-core::git::Commit`
@@ -15,13 +16,9 @@ impl<'repo> CommitExt for git2::Commit<'repo> {
1516
fn message_bstr(&self) -> &BStr {
1617
self.message_bytes().as_ref()
1718
}
19+
1820
fn change_id(&self) -> Option<String> {
19-
let cid = self.header_field_bytes("change-id").ok()?;
20-
if cid.is_empty() {
21-
None
22-
} else {
23-
String::from_utf8(cid.to_owned()).ok()
24-
}
21+
self.gitbutler_headers().map(|headers| headers.change_id)
2522
}
2623
fn is_signed(&self) -> bool {
2724
self.header_field_bytes("gpgsig").is_ok()
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
use bstr::{BStr, BString};
2+
use uuid::Uuid;
3+
4+
use super::CommitBuffer;
5+
6+
/// Header used to determine which version of the headers is in use. This should never be changed
7+
const HEADERS_VERSION_HEADER: &str = "gitbutler-headers-version";
8+
9+
const V1_CHANGE_ID_HEADER: &str = "change-id";
10+
11+
/// Used to represent the old commit headers layout. This should not be used in new code
12+
#[derive(Debug)]
13+
struct CommitHeadersV1 {
14+
change_id: String,
15+
}
16+
17+
/// The version number used to represent the V2 headers
18+
const V2_HEADERS_VERSION: &str = "2";
19+
20+
const V2_CHANGE_ID_HEADER: &str = "gitbutler-change-id";
21+
const V2_IS_UNAPPLIED_HEADER_COMMIT_HEADER: &str = "gitbutler-is-unapplied-header-commit";
22+
const V2_VBRANCH_NAME_HEADER: &str = "gitbutler-vbranch-name";
23+
#[derive(Debug)]
24+
pub struct CommitHeadersV2 {
25+
pub change_id: String,
26+
pub is_unapplied_header_commit: bool,
27+
pub vbranch_name: Option<String>,
28+
}
29+
30+
impl Default for CommitHeadersV2 {
31+
fn default() -> Self {
32+
CommitHeadersV2 {
33+
// Change ID using base16 encoding
34+
change_id: Uuid::new_v4().to_string(),
35+
is_unapplied_header_commit: false,
36+
vbranch_name: None,
37+
}
38+
}
39+
}
40+
41+
impl From<CommitHeadersV1> for CommitHeadersV2 {
42+
fn from(commit_headers_v1: CommitHeadersV1) -> CommitHeadersV2 {
43+
CommitHeadersV2 {
44+
change_id: commit_headers_v1.change_id,
45+
is_unapplied_header_commit: false,
46+
vbranch_name: None,
47+
}
48+
}
49+
}
50+
51+
pub trait HasCommitHeaders {
52+
fn gitbutler_headers(&self) -> Option<CommitHeadersV2>;
53+
}
54+
55+
impl HasCommitHeaders for git2::Commit<'_> {
56+
fn gitbutler_headers(&self) -> Option<CommitHeadersV2> {
57+
if let Ok(header) = self.header_field_bytes(HEADERS_VERSION_HEADER) {
58+
let version_number = BString::new(header.to_owned());
59+
60+
// Parse v2 headers
61+
if version_number == BStr::new(V2_HEADERS_VERSION) {
62+
let change_id = self.header_field_bytes(V2_CHANGE_ID_HEADER).ok()?;
63+
// We can safely assume that the change id should be UTF8
64+
let change_id = change_id.as_str()?.to_string();
65+
66+
// We can rationalize about is unapplied header commit with a bstring
67+
let is_wip_commit = self
68+
.header_field_bytes(V2_IS_UNAPPLIED_HEADER_COMMIT_HEADER)
69+
.ok()?;
70+
let is_wip_commit = BString::new(is_wip_commit.to_owned());
71+
72+
// We can safely assume that the vbranch name should be UTF8
73+
let vbranch_name = self
74+
.header_field_bytes(V2_VBRANCH_NAME_HEADER)
75+
.ok()
76+
.and_then(|buffer| Some(buffer.as_str()?.to_string()));
77+
78+
Some(CommitHeadersV2 {
79+
change_id,
80+
is_unapplied_header_commit: is_wip_commit == "true",
81+
vbranch_name,
82+
})
83+
} else {
84+
// Must be for a version we don't recognise
85+
None
86+
}
87+
} else {
88+
// Parse v1 headers
89+
let change_id = self.header_field_bytes(V1_CHANGE_ID_HEADER).ok()?;
90+
// We can safely assume that the change id should be UTF8
91+
let change_id = change_id.as_str()?.to_string();
92+
93+
let headers = CommitHeadersV1 { change_id };
94+
95+
Some(headers.into())
96+
}
97+
}
98+
}
99+
100+
impl CommitHeadersV2 {
101+
/// Used to create a CommitHeadersV2. This does not allow a change_id to be
102+
/// provided in order to ensure a consistent format.
103+
pub fn new(is_unapplied_header_commit: bool, vbranch_name: Option<String>) -> CommitHeadersV2 {
104+
CommitHeadersV2 {
105+
is_unapplied_header_commit,
106+
vbranch_name,
107+
..Default::default()
108+
}
109+
}
110+
111+
pub fn inject_default(commit_buffer: &mut CommitBuffer) {
112+
CommitHeadersV2::default().inject_into(commit_buffer)
113+
}
114+
115+
pub fn inject_into(&self, commit_buffer: &mut CommitBuffer) {
116+
commit_buffer.set_header(HEADERS_VERSION_HEADER, V2_HEADERS_VERSION);
117+
commit_buffer.set_header(V2_CHANGE_ID_HEADER, &self.change_id);
118+
let is_unapplied_header_commit = if self.is_unapplied_header_commit {
119+
"true"
120+
} else {
121+
"false"
122+
};
123+
commit_buffer.set_header(
124+
V2_IS_UNAPPLIED_HEADER_COMMIT_HEADER,
125+
is_unapplied_header_commit,
126+
);
127+
128+
if let Some(vbranch_name) = &self.vbranch_name {
129+
commit_buffer.set_header(V2_VBRANCH_NAME_HEADER, vbranch_name);
130+
};
131+
}
132+
}

crates/gitbutler-core/src/git/mod.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,5 +19,8 @@ pub use commit_ext::*;
1919
mod commit_buffer;
2020
pub use commit_buffer::*;
2121

22+
mod commit_headers;
23+
pub use commit_headers::*;
24+
2225
mod branch_ext;
2326
pub use branch_ext::*;

crates/gitbutler-core/src/git/repository_ext.rs

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ use crate::{
88
error::Code,
99
};
1010

11-
use super::{CommitBuffer, Refname};
11+
use super::{CommitBuffer, CommitHeadersV2, Refname};
1212
use std::io::Write;
1313
#[cfg(unix)]
1414
use std::os::unix::fs::PermissionsExt;
@@ -55,7 +55,7 @@ pub trait RepositoryExt {
5555
message: &str,
5656
tree: &git2::Tree<'_>,
5757
parents: &[&git2::Commit<'_>],
58-
change_id: Option<&str>,
58+
commit_headers: Option<CommitHeadersV2>,
5959
) -> Result<git2::Oid>;
6060

6161
fn blame(
@@ -161,19 +161,23 @@ impl RepositoryExt for Repository {
161161
message: &str,
162162
tree: &git2::Tree<'_>,
163163
parents: &[&git2::Commit<'_>],
164-
change_id: Option<&str>,
164+
commit_headers: Option<CommitHeadersV2>,
165165
) -> Result<git2::Oid> {
166166
let mut commit_buffer: CommitBuffer = self
167167
.commit_create_buffer(author, committer, message, tree, parents)?
168-
.try_into()?;
168+
.into();
169169

170-
commit_buffer.set_change_id(change_id);
170+
commit_buffer.set_gitbutler_headers(commit_headers);
171171

172172
let oid = if self.gb_config()?.sign_commits.unwrap_or(false) {
173173
let signature = self.sign_buffer(&commit_buffer);
174174
match signature {
175175
Ok(signature) => self
176-
.commit_signed(commit_buffer.as_string().as_ref(), &signature, None)
176+
.commit_signed(
177+
commit_buffer.as_bstring().to_string().as_str(),
178+
&signature,
179+
None,
180+
)
177181
.map_err(Into::into),
178182
Err(e) => {
179183
// If signing fails, set the "gitbutler.signCommits" config to false before erroring out
@@ -195,10 +199,9 @@ impl RepositoryExt for Repository {
195199
}
196200

197201
fn commit_buffer(&self, commit_buffer: &CommitBuffer) -> Result<git2::Oid> {
198-
let oid = self.odb()?.write(
199-
git2::ObjectType::Commit,
200-
commit_buffer.as_string().as_bytes(),
201-
)?;
202+
let oid = self
203+
.odb()?
204+
.write(git2::ObjectType::Commit, &commit_buffer.as_bstring())?;
202205

203206
Ok(oid)
204207
}
@@ -235,7 +238,7 @@ impl RepositoryExt for Repository {
235238
if is_ssh {
236239
// write commit data to a temp file so we can sign it
237240
let mut signature_storage = tempfile::NamedTempFile::new()?;
238-
signature_storage.write_all(buffer.as_string().as_ref())?;
241+
signature_storage.write_all(&buffer.as_bstring())?;
239242
let buffer_file_to_sign_path = signature_storage.into_temp_path();
240243

241244
let gpg_program = self.config()?.get_string("gpg.ssh.program");
@@ -329,7 +332,7 @@ impl RepositoryExt for Repository {
329332
.stdin
330333
.take()
331334
.expect("configured")
332-
.write_all(buffer.as_string().as_ref())?;
335+
.write_all(&buffer.as_bstring())?;
333336

334337
let output = child.wait_with_output()?;
335338
if output.status.success() {

0 commit comments

Comments
 (0)