Skip to content

Commit

Permalink
Fix handling of commits with custom headers
Browse files Browse the repository at this point in the history
  • Loading branch information
bjeanes committed Sep 14, 2024
1 parent a3ae1f7 commit 8249b6a
Show file tree
Hide file tree
Showing 3 changed files with 215 additions and 32 deletions.
170 changes: 168 additions & 2 deletions josh-core/src/commit_buffer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,12 @@ impl CommitBuffer {
}
}

pub fn set_header(&mut self, key: &str, value: &str) {
pub fn set_header<K: AsRef<[u8]>, V: AsRef<[u8]>>(&mut self, key: K, value: V) {
let mut set_heading = false;
let key: BString = key.as_ref().into();
let value = value.as_ref();
self.heading.iter_mut().for_each(|(k, v)| {
if k == key {
if *k == key {
*v = value.into();
set_heading = true;
}
Expand All @@ -42,6 +44,59 @@ impl CommitBuffer {
}
}

pub fn remove_gpg_signature(&mut self) {
self.heading.retain(|(k, _)| k != "gpgsig");
}

// special handling for parents, because the header can appear multiple times and we want to replace all "parent"
// headers with new "parent" headers based on provided values, taking care to preserve the position of the headers
pub fn set_parents(&mut self, new_parents: &[&str]) {
if new_parents.is_empty() {
self.heading.retain(|(k, _)| k != "parent");
return;
}

let delete_token = "_delete_";
let mut insertion_index: usize = 0; // by default, we insert at the start of the heading
let mut new_parents = new_parents.into_iter();

self.heading
.iter_mut()
.enumerate()
.for_each(|(idx, (k, v))| {
if k == "tree" {
insertion_index = idx + 1;
} else if k == "parent" {
if let Some(new_parent) = new_parents.next() {
*v = BString::from(*new_parent);
insertion_index = idx + 1;
} else {
*v = BString::from(delete_token);
}
}
});

self.heading
.retain(|(k, v)| k != "parent" || v != delete_token);

self.heading.splice(
insertion_index..insertion_index,
new_parents.map(|p| ("parent".into(), BString::from(*p))),
);
}

pub fn set_committer(&mut self, signature: &git2::Signature) {
self.set_header("committer", &format_signature(signature));
}

pub fn set_author(&mut self, signature: &git2::Signature) {
self.set_header("author", &format_signature(signature));
}

pub fn set_message<B: AsRef<[u8]>>(&mut self, message: B) {
self.message = message.as_ref().into();
}

pub fn as_bstring(&self) -> BString {
let mut output = BString::new(vec![]);

Expand All @@ -59,6 +114,26 @@ impl CommitBuffer {
output
}
}
fn format_signature(signature: &git2::Signature) -> Vec<u8> {
let mut output = vec![];

let time = signature.when();
let offset = time.offset_minutes();

output.push_str(signature.name_bytes());
output.push_str(" <");
output.push_str(signature.email_bytes());
output.push_str("> ");
output.push_str(format!(
"{} {}{:02}{:02}",
time.seconds(),
time.sign(),
offset / 60,
offset % 60
));

output
}

impl From<git2::Buf> for CommitBuffer {
fn from(git2_buffer: git2::Buf) -> Self {
Expand Down Expand Up @@ -97,3 +172,94 @@ fn test_commit_buffer() {
);
}

#[test]
fn test_set_parents_setting_when_unset() {
let mut buffer = CommitBuffer::new(b"key value\n\nmessage");
buffer.set_parents(&["parent1", "parent2"]);
assert_eq!(
buffer.heading,
vec![
("parent".into(), "parent1".into()),
("parent".into(), "parent2".into()),
("key".into(), "value".into())
]
);
}

#[test]
fn test_set_parents_setting_when_unset_inserts_after_tree() {
let mut buffer =
CommitBuffer::new(b"tree 123\ncommitter bob <[email protected]> 1465496956 +0200\n\nmessage");
buffer.set_parents(&["parent1", "parent2"]);
assert_eq!(
buffer.heading,
vec![
("tree".into(), "123".into()),
("parent".into(), "parent1".into()),
("parent".into(), "parent2".into()),
(
"committer".into(),
"bob <[email protected]> 1465496956 +0200".into()
)
]
);
}

#[test]
fn test_set_parents_unsetting_when_set() {
let mut buffer = CommitBuffer::new(b"parent original\nkey value\n\nmessage");
buffer.set_parents(&[]);
assert_eq!(buffer.heading, vec![("key".into(), "value".into())]);
}

#[test]
fn test_set_parents_updating() {
let mut buffer = CommitBuffer::new(b"parent original\n\nmessage");
buffer.set_parents(&["parent1", "parent2"]);
assert_eq!(
buffer.heading,
vec![
("parent".into(), "parent1".into()),
("parent".into(), "parent2".into()),
]
);
buffer.set_parents(&["parent3"]);
assert_eq!(buffer.heading, vec![("parent".into(), "parent3".into()),]);
}

#[test]
fn test_set_parents_updating_preserves_location_as_much_as_possible() {
let mut buffer = CommitBuffer::new(b"a b\nparent a\nc d\nparent b\ne f\n\nmessage");
buffer.set_parents(&["parent1", "parent2"]);
assert_eq!(
buffer.heading,
vec![
("a".into(), "b".into()),
("parent".into(), "parent1".into()),
("c".into(), "d".into()),
("parent".into(), "parent2".into()),
("e".into(), "f".into()),
]
);
buffer.set_parents(&["parent3"]);
assert_eq!(
buffer.heading,
vec![
("a".into(), "b".into()),
("parent".into(), "parent3".into()),
("c".into(), "d".into()),
("e".into(), "f".into()),
]
);
buffer.set_parents(&["parent1", "parent2"]);
assert_eq!(
buffer.heading,
vec![
("a".into(), "b".into()),
("parent".into(), "parent1".into()),
("parent".into(), "parent2".into()),
("c".into(), "d".into()),
("e".into(), "f".into()),
]
);
}
53 changes: 32 additions & 21 deletions josh-core/src/history.rs
Original file line number Diff line number Diff line change
Expand Up @@ -196,39 +196,50 @@ pub struct RewriteData<'a> {
pub message: Option<String>,
}

// takes everything from base except it's tree and replaces it with the tree
// takes everything from base except its tree and replaces it with the tree
// given
pub fn rewrite_commit(
repo: &git2::Repository,
base: &git2::Commit,
base: &git2::Commit, // original commit that we are modifying
parents: &[&git2::Commit],
rewrite_data: RewriteData,
unsign: bool,
) -> JoshResult<git2::Oid> {
use commit_buffer::CommitBuffer;

let message = rewrite_data
.message
.unwrap_or(base.message_raw().unwrap_or("no message").to_string());
let tree = &rewrite_data.tree;
let odb = repo.odb()?;
let odb_commit = odb.read(base.id())?;
assert!(odb_commit.kind() == git2::ObjectType::Commit);

let a = base.author();
let new_a = if let Some((author, email)) = rewrite_data.author {
git2::Signature::new(&author, &email, &a.when())?
} else {
a
};
let mut b = CommitBuffer::new(odb_commit.data());
b.set_header("tree", rewrite_data.tree.id().to_string().as_str());

let c = base.committer();
let new_c = if let Some((committer, email)) = rewrite_data.committer {
git2::Signature::new(&committer, &email, &c.when())?
} else {
c
};
let parent_shas: Vec<_> = parents.iter().map(|x| x.id().to_string()).collect();
b.set_parents(
parent_shas
.iter()
.map(|s| s.as_str())
.collect::<Vec<&str>>()
.as_slice(),
);

if let Some(message) = rewrite_data.message {
b.set_message(&message);
}

if let Some((author, email)) = rewrite_data.author {
let a = base.author();
let new_a = git2::Signature::new(&author, &email, &a.when())?;
b.set_author(&new_a);
}

if let Some((committer, email)) = rewrite_data.committer {
let a = base.committer();
let new_a = git2::Signature::new(&committer, &email, &a.when())?;
b.set_committer(&new_a);
}

let b: CommitBuffer = repo
.commit_create_buffer(&new_a, &new_c, &message, tree, parents)?
.into();
b.remove_gpg_signature();

if let (false, Ok((sig, _))) = (unsign, repo.extract_signature(&base.id(), None)) {
// Re-create the object with the original signature (which of course does not match any
Expand Down
24 changes: 15 additions & 9 deletions tests/proxy/roundtrip_custom_header.t
Original file line number Diff line number Diff line change
Expand Up @@ -62,15 +62,15 @@ Write a custom header into the commit (h/t https://github.com/Byron/gitoxide/blo
remote: upstream: response body:
remote:
remote: To http://localhost:8001/real_repo.git
remote: bb282e9..xxxxxxx JOSH_PUSH -> master
remote: bb282e9..edf5151 JOSH_PUSH -> master
To http://localhost:8002/real_repo.git:prefix=pre.git
1f0b9d8..c74f96c master -> master

$ cd ${TESTTMP}/real_repo
$ git pull --rebase
From http://localhost:8001/real_repo
bb282e9..xxxxxxx master -> origin/master
Updating bb282e9..xxxxxxx
bb282e9..edf5151 master -> origin/master
Updating bb282e9..edf5151
Fast-forward
file2 | 1 +
1 file changed, 1 insertion(+)
Expand All @@ -85,7 +85,7 @@ Write a custom header into the commit (h/t https://github.com/Byron/gitoxide/blo
2 directories, 2 files

$ git log --oneline --graph
* xxxxxxx add file2
* edf5151 add file2
* bb282e9 add file1

Re-clone to verify that the rewritten commit c74f96c is restored and the custom headers are preserved
Expand Down Expand Up @@ -125,14 +125,20 @@ Re-clone to verify that the rewritten commit c74f96c is restored and the custom
| |-- info
| | `-- exclude
| |-- objects
| | |-- 0f
| | | `-- 17ab2c89a1278ecb6a7438e915e491884d3efb
| | |-- 3d
| | | `-- 77ff51363c9825cc2a221fc0ba5a883a1a2c72
| | |-- 6b
| | | `-- 46faacade805991bcaea19382c9d941828ce80
| | |-- a0
| | | `-- 24003ee1acc6bf70318a46e7b6df651b9dc246
| | |-- bb
| | | `-- 282e9cdc1b972fffd08fd21eead43bc0c83cb8
| | |-- c8
| | | `-- 2fc150c43f13cc56c0e9caeba01b58ec612022
| | |-- ed
| | | `-- f51518ffef5a69791a6e38a6656068aeb2cf8e
| | |-- info
| | `-- pack
| `-- refs
Expand Down Expand Up @@ -162,15 +168,15 @@ Re-clone to verify that the rewritten commit c74f96c is restored and the custom
| | `-- 46faacade805991bcaea19382c9d941828ce80
| |-- b5
| | `-- af4d1258141efaadc32e369f4dc4b1f6c524e4
| |-- b7
| | `-- cf821182baff3432190af3ae2f1029d8e7ceb0
| |-- f9
| | `-- 9a2dde698e6d3fc31cbeedea6d0204399f90ce
| |-- c7
| | `-- 4f96c02a8f34cd4321d646e728aeb11ea34932
| |-- ed
| | `-- f51518ffef5a69791a6e38a6656068aeb2cf8e
| |-- info
| `-- pack
`-- refs
|-- heads
|-- namespaces
`-- tags

38 directories, 24 files
41 directories, 27 files

0 comments on commit 8249b6a

Please sign in to comment.