diff --git a/Cargo.lock b/Cargo.lock index 82064e59..27f7a4c1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1563,8 +1563,10 @@ version = "22.4.15" dependencies = [ "backtrace", "bitvec", + "bstr", "git-version", "git2", + "gix-object", "glob", "hex", "indoc", diff --git a/josh-core/Cargo.toml b/josh-core/Cargo.toml index 9bf4a839..f48a77e0 100644 --- a/josh-core/Cargo.toml +++ b/josh-core/Cargo.toml @@ -12,8 +12,10 @@ edition = "2021" [dependencies] backtrace = "0.3.72" bitvec = "1.0.1" +bstr = "1.9.0" git-version = "0.3.9" git2 = { workspace = true } +gix-object = "0.42.2" glob = "0.3.1" hex = "0.4.3" indoc = "2.0.5" diff --git a/josh-core/src/history.rs b/josh-core/src/history.rs index a0a339af..a632dffd 100644 --- a/josh-core/src/history.rs +++ b/josh-core/src/history.rs @@ -196,7 +196,7 @@ pub struct RewriteData<'a> { pub message: Option, } -// 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, @@ -205,40 +205,49 @@ pub fn rewrite_commit( rewrite_data: RewriteData, unsign: bool, ) -> JoshResult { - let message = rewrite_data - .message - .unwrap_or(base.message_raw().unwrap_or("no message").to_string()); - let tree = &rewrite_data.tree; - - 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 odb = repo.odb()?; + let odb_commit = odb.read(base.id())?; + assert!(odb_commit.kind() == git2::ObjectType::Commit); + + // gix_object uses byte strings for Oids, but in hex representation, not raw bytes. Its `Format` implementation + // writes out hex-encoded bytes. Because CommitRef's reference lifetimes we have to this, before creating CommitRef + let tree_id = format!("{}", rewrite_data.tree.id()); + let parent_ids = parents + .iter() + .map(|x| format!("{}", x.id())) + .collect::>(); - 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 mut commit = gix_object::CommitRef::from_bytes(odb_commit.data())?; + + commit.tree = tree_id.as_bytes().into(); + + commit.parents.clear(); + commit + .parents + .extend(parent_ids.iter().map(|x| x.as_bytes().into())); - let b = repo.commit_create_buffer(&new_a, &new_c, &message, tree, parents)?; - - 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 - // more, but this is needed to guarantee perfect round-trips). - let b = b - .as_str() - .ok_or_else(|| josh_error("non-UTF-8 signed commit"))?; - let sig = sig - .as_str() - .ok_or_else(|| josh_error("non-UTF-8 signature"))?; - return Ok(repo.commit_signed(b, sig, None)?); + if let Some(ref msg) = rewrite_data.message { + commit.message = msg.as_bytes().into(); } - return Ok(repo.odb()?.write(git2::ObjectType::Commit, &b)?); + if let Some((ref author, ref email)) = rewrite_data.author { + commit.author.name = author.as_bytes().into(); + commit.author.email = email.as_bytes().into(); + } + + if let Some((ref author, ref email)) = rewrite_data.committer { + commit.committer.name = author.as_bytes().into(); + commit.committer.email = email.as_bytes().into(); + } + + commit + .extra_headers + .retain(|(k, _)| *k != "gpgsig".as_bytes() || !unsign); + + let mut b = vec![]; + gix_object::WriteTo::write_to(&commit, &mut b)?; + + return Ok(odb.write(git2::ObjectType::Commit, &b)?); } // Given an OID of an unfiltered commit and a filter, diff --git a/run-tests.sh b/run-tests.sh index 2731d186..9747c532 100644 --- a/run-tests.sh +++ b/run-tests.sh @@ -38,4 +38,5 @@ export GIT_CONFIG_GLOBAL=${CONFIG_FILE} git config --global init.defaultBranch master cargo fmt +export RUST_BACKTRACE=1 python3 -m prysk "$@" diff --git a/tests/.gitignore b/tests/.gitignore new file mode 100644 index 00000000..d2bfb7e0 --- /dev/null +++ b/tests/.gitignore @@ -0,0 +1 @@ +*.t.err diff --git a/tests/filter/roundtrip_custom_header.t b/tests/filter/roundtrip_custom_header.t new file mode 100644 index 00000000..919e5788 --- /dev/null +++ b/tests/filter/roundtrip_custom_header.t @@ -0,0 +1,132 @@ + $ export TESTTMP=${PWD} + + $ cd ${TESTTMP} + $ git init -q repo 1>/dev/null + $ cd repo + + $ echo "hello world" > hw.txt + $ git add . + $ git commit -m initial + [master (root-commit) 7d7c929] initial + 1 file changed, 1 insertion(+) + create mode 100644 hw.txt + + $ mkdir subdir + $ echo "hello moon" > subdir/hw.txt + $ git add . + $ git commit -m second + [master bab39f4] second + 1 file changed, 1 insertion(+) + create mode 100644 subdir/hw.txt + + $ git diff ${EMPTY_TREE}..refs/heads/master + diff --git a/hw.txt b/hw.txt + new file mode 100644 + index 0000000..3b18e51 + --- /dev/null + +++ b/hw.txt + @@ -0,0 +1 @@ + +hello world + diff --git a/subdir/hw.txt b/subdir/hw.txt + new file mode 100644 + index 0000000..1b95c6e + --- /dev/null + +++ b/subdir/hw.txt + @@ -0,0 +1 @@ + +hello moon + +Write a custom header into the commit (h/t https://github.com/Byron/gitoxide/blob/68cbea8gix/tests/fixtures/make_pre_epoch_repo.sh#L12-L27) + $ git cat-file -p @ | tee commit.txt + tree 15e3a4de9f0b90057746be6658b0f321f4bcc470 + parent 7d7c9293be5483ccd1a24bdf33ad52cf07cda738 + author Josh 1112911993 +0000 + committer Josh 1112911993 +0000 + + second + + $ patch -p1 < diff --git a/commit.txt b/commit.txt + > index 1758866..fe1998a 100644 + > --- a/commit.txt + > +++ b/commit.txt + > @@ -2,5 +2,9 @@ tree 15e3a4de9f0b90057746be6658b0f321f4bcc470 + > parent 7d7c9293be5483ccd1a24bdf33ad52cf07cda738 + > author Josh 1112911993 +0000 + > committer Josh 1112911993 +0000 + > +custom-header with + > + multiline + > + value + > +another-header such that it sorts before custom-header + > + > second + > EOF + patching file commit.txt + $ new_commit=$(git hash-object --literally -w -t commit commit.txt) + $ echo $new_commit + f2fd7b23a4a2318d534d122615a6e75196c3e3c4 + $ git update-ref refs/heads/master $new_commit + + $ josh-filter --update refs/heads/filtered ':prefix=pre' + + $ git diff ${EMPTY_TREE}..refs/heads/filtered + diff --git a/pre/hw.txt b/pre/hw.txt + new file mode 100644 + index 0000000..3b18e51 + --- /dev/null + +++ b/pre/hw.txt + @@ -0,0 +1 @@ + +hello world + diff --git a/pre/subdir/hw.txt b/pre/subdir/hw.txt + new file mode 100644 + index 0000000..1b95c6e + --- /dev/null + +++ b/pre/subdir/hw.txt + @@ -0,0 +1 @@ + +hello moon + + $ git cat-file -p refs/heads/filtered + tree 6876aad1a2259b9d4c7c24e0e3ff908d3d580404 + parent 73007fa33b8628d6560b78e37191c07c9e001d3b + author Josh 1112911993 +0000 + committer Josh 1112911993 +0000 + custom-header with + multiline + value + another-header such that it sorts before custom-header + + second + + $ josh-filter --update refs/heads/re-filtered ':/pre' refs/heads/filtered + + $ git show refs/heads/re-filtered + commit f2fd7b23a4a2318d534d122615a6e75196c3e3c4 + Author: Josh + Date: Thu Apr 7 22:13:13 2005 +0000 + + second + + diff --git a/subdir/hw.txt b/subdir/hw.txt + new file mode 100644 + index 0000000..1b95c6e + --- /dev/null + +++ b/subdir/hw.txt + @@ -0,0 +1 @@ + +hello moon + + $ git cat-file -p refs/heads/re-filtered + tree 15e3a4de9f0b90057746be6658b0f321f4bcc470 + parent 7d7c9293be5483ccd1a24bdf33ad52cf07cda738 + author Josh 1112911993 +0000 + committer Josh 1112911993 +0000 + custom-header with + multiline + value + another-header such that it sorts before custom-header + + second + + $ git log --oneline --all --decorate + 63982dc (filtered) second + f2fd7b2 (HEAD -> master, re-filtered) second + 73007fa initial + 7d7c929 initial diff --git a/tests/proxy/get_version.t b/tests/proxy/get_version.t index c7847b5e..9604d23a 100644 --- a/tests/proxy/get_version.t +++ b/tests/proxy/get_version.t @@ -2,7 +2,7 @@ $ cd ${TESTTMP} $ curl -s http://localhost:8002/version - Version: r*.*.* (glob) + Version: v*.*.* (glob) $ bash ${TESTDIR}/destroy_test_env.sh .