diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index d9df2f20d5..dce7e79581 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -35,6 +35,7 @@ jobs: shell: bash - run: cargo test --locked - run: cargo test --features https,ssh + - run: cargo test --features unstable-sha256 - run: cargo run -p systest - run: cargo run -p systest --features unstable-sha256 - run: cargo test -p git2-curl diff --git a/Cargo.toml b/Cargo.toml index aaac74bac5..3a56de05ea 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,6 +34,18 @@ url = "2.5.4" [features] unstable = [] +# Experimental SHA256 OID support, +# reflecting upstream libgit2's GIT_EXPERIMENTAL_SHA256. +# +# This is an ABI-breaking change. +# Future releases with this feature may introduce breakages without notice +# Use at your own risk. +# +# Library authors: +# DO NOT enable this feature by default in your dependencies. +# Due to Cargo's additive features, +# downstream users cannot deactivate it once enabled. +unstable-sha256 = ["libgit2-sys/unstable-sha256"] default = [] ssh = ["libgit2-sys/ssh", "cred"] https = ["libgit2-sys/https", "openssl-sys", "openssl-probe", "cred"] diff --git a/examples/diff.rs b/examples/diff.rs index 7440149ba0..afc0767a5a 100644 --- a/examples/diff.rs +++ b/examples/diff.rs @@ -319,7 +319,13 @@ fn tree_to_treeish<'a>( fn resolve_blob<'a>(repo: &'a Repository, arg: Option<&String>) -> Result>, Error> { let arg = match arg { - Some(s) => Oid::from_str(s)?, + Some(s) => { + #[cfg(not(feature = "unstable-sha256"))] + let oid = Oid::from_str(s)?; + #[cfg(feature = "unstable-sha256")] + let oid = Oid::from_str(s, repo.object_format())?; + oid + } None => return Ok(None), }; repo.find_blob(arg).map(|b| Some(b)) diff --git a/examples/init.rs b/examples/init.rs index 3ae79082d7..861bd9d370 100644 --- a/examples/init.rs +++ b/examples/init.rs @@ -15,6 +15,7 @@ #![deny(warnings)] use clap::Parser; +use git2::ObjectFormat; use git2::{Error, Repository, RepositoryInitMode, RepositoryInitOptions}; use std::path::{Path, PathBuf}; @@ -40,6 +41,9 @@ struct Args { #[structopt(name = "perms", long = "shared")] /// permissions to create the repository with flag_shared: Option, + #[structopt(name = "object-format", long, value_parser = parse_object_format)] + /// object format to use (sha1 or sha256, requires unstable-sha256 feature) + flag_object_format: Option, } fn run(args: &Args) -> Result<(), Error> { @@ -48,6 +52,7 @@ fn run(args: &Args) -> Result<(), Error> { && args.flag_template.is_none() && args.flag_shared.is_none() && args.flag_separate_git_dir.is_none() + && args.flag_object_format.is_none() { Repository::init(&path)? } else { @@ -68,6 +73,12 @@ fn run(args: &Args) -> Result<(), Error> { if let Some(ref s) = args.flag_shared { opts.mode(parse_shared(s)?); } + + #[cfg(feature = "unstable-sha256")] + if let Some(format) = args.flag_object_format { + opts.object_format(format); + } + Repository::init_opts(&path, &opts)? }; @@ -136,6 +147,15 @@ fn parse_shared(shared: &str) -> Result { } } +fn parse_object_format(format: &str) -> Result { + match format { + "sha1" => Ok(ObjectFormat::Sha1), + #[cfg(feature = "unstable-sha256")] + "sha256" => Ok(ObjectFormat::Sha256), + _ => Err(Error::from_str("object format must be 'sha1' or 'sha256'")), + } +} + fn main() { let args = Args::parse(); match run(&args) { diff --git a/libgit2-sys/lib.rs b/libgit2-sys/lib.rs index a8e991a589..fd4999b929 100644 --- a/libgit2-sys/lib.rs +++ b/libgit2-sys/lib.rs @@ -2304,6 +2304,8 @@ extern "C" { pub fn git_repository_index(out: *mut *mut git_index, repo: *mut git_repository) -> c_int; pub fn git_repository_set_index(repo: *mut git_repository, index: *mut git_index) -> c_int; + pub fn git_repository_oid_type(repo: *mut git_repository) -> git_oid_t; + pub fn git_repository_message(buf: *mut git_buf, repo: *mut git_repository) -> c_int; pub fn git_repository_message_remove(repo: *mut git_repository) -> c_int; @@ -2435,7 +2437,9 @@ extern "C" { pub fn git_oid_cmp(a: *const git_oid, b: *const git_oid) -> c_int; pub fn git_oid_equal(a: *const git_oid, b: *const git_oid) -> c_int; pub fn git_oid_streq(id: *const git_oid, str: *const c_char) -> c_int; + #[deprecated = "use `git_oid_is_zero`"] pub fn git_oid_iszero(id: *const git_oid) -> c_int; + pub fn git_oid_is_zero(id: *const git_oid) -> c_int; // error pub fn git_error_last() -> *const git_error; diff --git a/src/blob.rs b/src/blob.rs index e72abbe262..f11c76dd1b 100644 --- a/src/blob.rs +++ b/src/blob.rs @@ -95,9 +95,7 @@ impl<'repo> BlobWriter<'repo> { pub fn commit(mut self) -> Result { // After commit we already doesn't need cleanup on drop self.need_cleanup = false; - let mut raw = raw::git_oid { - id: [0; raw::GIT_OID_MAX_SIZE], - }; + let mut raw = crate::util::zeroed_raw_oid(); unsafe { try_call!(raw::git_blob_create_fromstream_commit(&mut raw, self.raw)); Ok(Binding::from_raw(&raw as *const _)) diff --git a/src/build.rs b/src/build.rs index eebbdd5ebe..10083063ff 100644 --- a/src/build.rs +++ b/src/build.rs @@ -723,9 +723,7 @@ impl TreeUpdateBuilder { self.paths.push(path); self.updates.push(raw::git_tree_update { action: raw::GIT_TREE_UPDATE_REMOVE, - id: raw::git_oid { - id: [0; raw::GIT_OID_MAX_SIZE], - }, + id: crate::util::zeroed_raw_oid(), filemode: raw::GIT_FILEMODE_UNREADABLE, path: path_ptr, }); @@ -754,9 +752,7 @@ impl TreeUpdateBuilder { /// /// The baseline tree must exist in the specified repository. pub fn create_updated(&mut self, repo: &Repository, baseline: &Tree<'_>) -> Result { - let mut ret = raw::git_oid { - id: [0; raw::GIT_OID_MAX_SIZE], - }; + let mut ret = crate::util::zeroed_raw_oid(); unsafe { try_call!(raw::git_tree_create_updated( &mut ret, diff --git a/src/commit.rs b/src/commit.rs index 7095bccf94..9e0cafdd65 100644 --- a/src/commit.rs +++ b/src/commit.rs @@ -260,9 +260,7 @@ impl<'repo> Commit<'repo> { message: Option<&str>, tree: Option<&Tree<'repo>>, ) -> Result { - let mut raw = raw::git_oid { - id: [0; raw::GIT_OID_MAX_SIZE], - }; + let mut raw = crate::util::zeroed_raw_oid(); let update_ref = crate::opt_cstr(update_ref)?; let encoding = crate::opt_cstr(message_encoding)?; let message = crate::opt_cstr(message)?; @@ -439,10 +437,15 @@ mod tests { assert_eq!(commit.parents().count(), 0); let tree_header_bytes = commit.header_field_bytes("tree").unwrap(); - assert_eq!( - crate::Oid::from_str(tree_header_bytes.as_str().unwrap()).unwrap(), - commit.tree_id() - ); + let tree_oid = { + let str = tree_header_bytes.as_str().unwrap(); + #[cfg(not(feature = "unstable-sha256"))] + let oid = crate::Oid::from_str(str).unwrap(); + #[cfg(feature = "unstable-sha256")] + let oid = crate::Oid::from_str(str, repo.object_format()).unwrap(); + oid + }; + assert_eq!(tree_oid, commit.tree_id()); assert_eq!(commit.author().name(), Some("name")); assert_eq!(commit.author().email(), Some("email")); assert_eq!(commit.committer().name(), Some("name")); @@ -469,4 +472,45 @@ mod tests { .ok() .unwrap(); } + + #[test] + #[cfg(feature = "unstable-sha256")] + fn smoke_sha256() { + let (_td, repo) = crate::test::repo_init_sha256(); + let head = repo.head().unwrap(); + let target = head.target().unwrap(); + let commit = repo.find_commit(target).unwrap(); + + // Verify SHA256 OID (32 bytes) + assert_eq!(commit.id().as_bytes().len(), 32); + assert_eq!(commit.tree_id().as_bytes().len(), 32); + + assert_eq!(commit.message(), Some("initial\n\nbody")); + assert_eq!(commit.body(), Some("body")); + assert_eq!(commit.id(), target); + commit.summary().unwrap(); + commit.tree().unwrap(); + assert_eq!(commit.parents().count(), 0); + + let tree_header_bytes = commit.header_field_bytes("tree").unwrap(); + let tree_oid = { + let str = tree_header_bytes.as_str().unwrap(); + let oid = crate::Oid::from_str(str, repo.object_format()).unwrap(); + oid + }; + assert_eq!(tree_oid, commit.tree_id()); + + // Create child commit with parent + let sig = repo.signature().unwrap(); + let tree = repo.find_tree(commit.tree_id()).unwrap(); + let id = repo + .commit(Some("HEAD"), &sig, &sig, "bar", &tree, &[&commit]) + .unwrap(); + let head = repo.find_commit(id).unwrap(); + + // Verify child commit ID is also SHA256 + assert_eq!(head.id().as_bytes().len(), 32); + assert_eq!(head.parent_count(), 1); + assert_eq!(head.parent_id(0).unwrap(), commit.id()); + } } diff --git a/src/diff.rs b/src/diff.rs index 5ba3535a9b..f52627f87a 100644 --- a/src/diff.rs +++ b/src/diff.rs @@ -289,9 +289,7 @@ impl<'repo> Diff<'repo> { /// Create a patch ID from a diff. pub fn patchid(&self, opts: Option<&mut DiffPatchidOptions>) -> Result { - let mut raw = raw::git_oid { - id: [0; raw::GIT_OID_MAX_SIZE], - }; + let mut raw = crate::util::zeroed_raw_oid(); unsafe { try_call!(raw::git_diff_patchid( &mut raw, @@ -312,16 +310,25 @@ impl Diff<'static> { /// two trees, however there may be subtle differences. For example, /// a patch file likely contains abbreviated object IDs, so the /// object IDs parsed by this function will also be abbreviated. - pub fn from_buffer(buffer: &[u8]) -> Result, Error> { + pub fn from_buffer( + buffer: &[u8], + #[cfg(feature = "unstable-sha256")] format: crate::ObjectFormat, + ) -> Result, Error> { crate::init(); let mut diff: *mut raw::git_diff = std::ptr::null_mut(); + let data = buffer.as_ptr() as *const c_char; + let len = buffer.len(); unsafe { // NOTE: Doesn't depend on repo, so lifetime can be 'static - try_call!(raw::git_diff_from_buffer( - &mut diff, - buffer.as_ptr() as *const c_char, - buffer.len() - )); + #[cfg(not(feature = "unstable-sha256"))] + try_call!(raw::git_diff_from_buffer(&mut diff, data, len)); + #[cfg(feature = "unstable-sha256")] + { + let mut opts: raw::git_diff_parse_options = std::mem::zeroed(); + opts.version = raw::GIT_DIFF_PARSE_OPTIONS_VERSION; + opts.oid_type = format.raw(); + try_call!(raw::git_diff_from_buffer(&mut diff, data, len, &mut opts)); + } Ok(Diff::from_raw(diff)) } } @@ -1554,6 +1561,8 @@ impl DiffPatchidOptions { #[cfg(test)] mod tests { + #[cfg(feature = "unstable-sha256")] + use crate::Diff; use crate::{DiffLineType, DiffOptions, Oid, Signature, Time}; use std::borrow::Borrow; use std::fs::File; @@ -1860,4 +1869,37 @@ mod tests { assert_eq!(result.unwrap_err().code(), crate::ErrorCode::User); } + + #[test] + #[cfg(feature = "unstable-sha256")] + fn diff_sha256() { + let (_td, repo) = crate::test::repo_init_sha256(); + let diff = repo.diff_tree_to_workdir(None, None).unwrap(); + assert_eq!(diff.deltas().len(), 0); + let stats = diff.stats().unwrap(); + assert_eq!(stats.insertions(), 0); + assert_eq!(stats.deletions(), 0); + assert_eq!(stats.files_changed(), 0); + let patchid = diff.patchid(None).unwrap(); + + // Verify SHA256 OID (32 bytes) + assert_eq!(patchid.as_bytes().len(), 32); + } + + #[test] + #[cfg(feature = "unstable-sha256")] + fn diff_from_buffer_sha256() { + // Minimal patch with SHA256 OID (64 chars) + let patch = b"diff --git a/file.txt b/file.txt +index 0000000000000000000000000000000000000000000000000000000000000000..1111111111111111111111111111111111111111111111111111111111111111 100644 +--- a/file.txt ++++ b/file.txt +@@ -1 +1 @@ +-old ++new +"; + + let diff = Diff::from_buffer(patch, crate::ObjectFormat::Sha256).unwrap(); + assert_eq!(diff.deltas().len(), 1); + } } diff --git a/src/index.rs b/src/index.rs index d02b45e169..e2000f0752 100644 --- a/src/index.rs +++ b/src/index.rs @@ -90,6 +90,23 @@ impl Index { /// /// This index object cannot be read/written to the filesystem, but may be /// used to perform in-memory index operations. + /// + ///
+ /// + /// # SHA1-only limitation + /// + /// This method **always** creates a SHA1 index. + /// + /// In future releases, this will be removed entirely to avoid misuse. + /// + /// Consider these alternatives: + /// + /// * [`Index::with_object_format`] if an in-memory index is needed + /// * [`Repository::index`] if you have repository context + /// + ///
+ #[cfg(not(feature = "unstable-sha256"))] + #[deprecated = "this always creates a SHA1 index, consider using `Index::with_object_format`"] pub fn new() -> Result { crate::init(); let mut raw = ptr::null_mut(); @@ -99,6 +116,30 @@ impl Index { } } + /// Creates a new in-memory index with the specified object format. + /// + /// This index object cannot be read/written to the filesystem, but may be + /// used to perform in-memory index operations. + pub fn with_object_format(format: crate::ObjectFormat) -> Result { + crate::init(); + let mut raw = ptr::null_mut(); + unsafe { + #[cfg(not(feature = "unstable-sha256"))] + { + let _ = format; + try_call!(raw::git_index_new(&mut raw)); + } + #[cfg(feature = "unstable-sha256")] + { + let mut opts: raw::git_index_options = std::mem::zeroed(); + opts.version = raw::GIT_INDEX_OPTIONS_VERSION; + opts.oid_type = format.raw(); + try_call!(raw::git_index_new(&mut raw, &opts)); + } + Ok(Binding::from_raw(raw)) + } + } + /// Create a new bare Git index object as a memory representation of the Git /// index file in 'index_path', without a repository to back it. /// @@ -107,13 +148,24 @@ impl Index { /// /// If you need an index attached to a repository, use the `index()` method /// on `Repository`. - pub fn open(index_path: &Path) -> Result { + pub fn open( + index_path: &Path, + #[cfg(feature = "unstable-sha256")] format: crate::ObjectFormat, + ) -> Result { crate::init(); let mut raw = ptr::null_mut(); // Normal file path OK (does not need Windows conversion). let index_path = index_path.into_c_string()?; unsafe { + #[cfg(not(feature = "unstable-sha256"))] try_call!(raw::git_index_open(&mut raw, index_path)); + #[cfg(feature = "unstable-sha256")] + { + let mut opts: raw::git_index_options = std::mem::zeroed(); + opts.version = raw::GIT_INDEX_OPTIONS_VERSION; + opts.oid_type = format.raw(); + try_call!(raw::git_index_open(&mut raw, index_path, &opts)); + } Ok(Binding::from_raw(raw)) } } @@ -616,9 +668,7 @@ impl Index { /// /// The index must not contain any file in conflict. pub fn write_tree(&mut self) -> Result { - let mut raw = raw::git_oid { - id: [0; raw::GIT_OID_MAX_SIZE], - }; + let mut raw = crate::util::zeroed_raw_oid(); unsafe { try_call!(raw::git_index_write_tree(&mut raw, self.raw)); Ok(Binding::from_raw(&raw as *const _)) @@ -630,9 +680,7 @@ impl Index { /// This is the same as `write_tree` except that the destination repository /// can be chosen. pub fn write_tree_to(&mut self, repo: &Repository) -> Result { - let mut raw = raw::git_oid { - id: [0; raw::GIT_OID_MAX_SIZE], - }; + let mut raw = crate::util::zeroed_raw_oid(); unsafe { try_call!(raw::git_index_write_tree_to(&mut raw, self.raw, repo.raw())); Ok(Binding::from_raw(&raw as *const _)) @@ -850,11 +898,12 @@ mod tests { use std::path::Path; use tempfile::TempDir; + use crate::ObjectFormat; use crate::{ErrorCode, Index, IndexEntry, IndexTime, Oid, Repository, ResetType}; #[test] fn smoke() { - let mut index = Index::new().unwrap(); + let mut index = Index::with_object_format(ObjectFormat::Sha1).unwrap(); assert!(index.add_path(&Path::new(".")).is_err()); index.clear().unwrap(); assert_eq!(index.len(), 0); @@ -871,7 +920,10 @@ mod tests { index.path().map(|s| s.to_path_buf()), Some(repo.path().join("index")) ); + #[cfg(not(feature = "unstable-sha256"))] Index::open(&repo.path().join("index")).unwrap(); + #[cfg(feature = "unstable-sha256")] + Index::open(&repo.path().join("index"), ObjectFormat::Sha1).unwrap(); index.clear().unwrap(); index.read(true).unwrap(); @@ -953,7 +1005,7 @@ mod tests { #[test] fn add_then_read() { - let mut index = Index::new().unwrap(); + let mut index = Index::with_object_format(ObjectFormat::Sha1).unwrap(); let mut e = entry(); e.path = b"foobar".to_vec(); index.add(&e).unwrap(); @@ -963,7 +1015,7 @@ mod tests { #[test] fn add_then_find() { - let mut index = Index::new().unwrap(); + let mut index = Index::with_object_format(ObjectFormat::Sha1).unwrap(); let mut e = entry(); e.path = b"foo/bar".to_vec(); index.add(&e).unwrap(); @@ -1008,10 +1060,38 @@ mod tests { uid: 0, gid: 0, file_size: 0, + #[cfg(not(feature = "unstable-sha256"))] id: Oid::from_bytes(&[0; 20]).unwrap(), + #[cfg(feature = "unstable-sha256")] + id: Oid::from_bytes(&[0; 32]).unwrap(), flags: 0, flags_extended: 0, path: Vec::new(), } } + + #[test] + #[cfg(feature = "unstable-sha256")] + fn index_sha256() { + let (_td, repo) = crate::test::repo_init_sha256(); + let mut index = repo.index().unwrap(); + + // Test opening with correct format + Index::open(&repo.path().join("index"), ObjectFormat::Sha256).unwrap(); + + // Test basic operations with SHA256 + index.clear().unwrap(); + index.read(true).unwrap(); + index.write().unwrap(); + let tree_id = index.write_tree().unwrap(); + + // Verify OID is 32 bytes (SHA256) + assert_eq!(tree_id.as_bytes().len(), 32); + } + + #[test] + #[cfg(feature = "unstable-sha256")] + fn smooke_in_memory_index_sha256() { + let _index = Index::with_object_format(ObjectFormat::Sha256).unwrap(); + } } diff --git a/src/indexer.rs b/src/indexer.rs index 3a3ff62a5a..e07a97a5d5 100644 --- a/src/indexer.rs +++ b/src/indexer.rs @@ -123,7 +123,13 @@ impl<'a> Indexer<'a> { /// `mode` is the permissions to use for the output files, use `0` for defaults. /// /// If `verify` is `false`, the indexer will bypass object connectivity checks. - pub fn new(odb: Option<&Odb<'a>>, path: &Path, mode: u32, verify: bool) -> Result { + pub fn new( + odb: Option<&Odb<'a>>, + path: &Path, + mode: u32, + verify: bool, + #[cfg(feature = "unstable-sha256")] format: crate::ObjectFormat, + ) -> Result { crate::init(); let path = path.into_c_string()?; @@ -144,7 +150,17 @@ impl<'a> Indexer<'a> { opts.progress_cb_payload = progress_payload_ptr as *mut c_void; opts.verify = verify.into(); + #[cfg(feature = "unstable-sha256")] + { + opts.mode = mode; + opts.oid_type = format.raw(); + opts.odb = odb; + } + + #[cfg(not(feature = "unstable-sha256"))] try_call!(raw::git_indexer_new(&mut out, path, mode, odb, &mut opts)); + #[cfg(feature = "unstable-sha256")] + try_call!(raw::git_indexer_new(&mut out, path, &mut opts)); } Ok(Self { @@ -238,6 +254,46 @@ mod tests { repo_target.path().join("objects").join("pack").as_path(), 0o644, true, + #[cfg(feature = "unstable-sha256")] + crate::ObjectFormat::Sha1, + ) + .unwrap(); + indexer.progress(|_| { + progress_called = true; + true + }); + indexer.write(&buf).unwrap(); + indexer.commit().unwrap(); + + // Assert that target repo picks it up as valid + let commit_target = repo_target.find_commit(commit_source_id).unwrap(); + assert_eq!(commit_target.id(), commit_source_id); + assert!(progress_called); + } + + #[test] + #[cfg(feature = "unstable-sha256")] + fn indexer_sha256() { + let (_td, repo_source) = crate::test::repo_init_sha256(); + let (_td, repo_target) = crate::test::repo_init_sha256(); + + let mut progress_called = false; + + // Create an in-memory packfile + let mut builder = t!(repo_source.packbuilder()); + let mut buf = Buf::new(); + let (commit_source_id, _tree) = crate::test::commit(&repo_source); + t!(builder.insert_object(commit_source_id, None)); + t!(builder.write_buf(&mut buf)); + + // Write it to the standard location in the target repo, but via indexer + let odb = repo_source.odb().unwrap(); + let mut indexer = Indexer::new( + Some(&odb), + repo_target.path().join("objects").join("pack").as_path(), + 0o644, + true, + crate::ObjectFormat::Sha256, ) .unwrap(); indexer.progress(|_| { diff --git a/src/lib.rs b/src/lib.rs index 675680a8e5..a29514a7ee 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -112,6 +112,7 @@ pub use crate::message::{ pub use crate::note::{Note, Notes}; pub use crate::object::Object; pub use crate::odb::{Odb, OdbObject, OdbPackwriter, OdbReader, OdbWriter}; +pub use crate::oid::ObjectFormat; pub use crate::oid::Oid; pub use crate::packbuilder::{PackBuilder, PackBuilderStage}; pub use crate::patch::Patch; diff --git a/src/note.rs b/src/note.rs index b0049226a9..821a7baf76 100644 --- a/src/note.rs +++ b/src/note.rs @@ -92,9 +92,7 @@ impl<'repo> Binding for Notes<'repo> { impl<'repo> Iterator for Notes<'repo> { type Item = Result<(Oid, Oid), Error>; fn next(&mut self) -> Option> { - let mut note_id = raw::git_oid { - id: [0; raw::GIT_OID_MAX_SIZE], - }; + let mut note_id = crate::util::zeroed_raw_oid(); let mut annotated_id = note_id; unsafe { try_call_iter!(raw::git_note_next( diff --git a/src/odb.rs b/src/odb.rs index 2e531dc58c..88799acdc6 100644 --- a/src/odb.rs +++ b/src/odb.rs @@ -45,6 +45,17 @@ impl<'repo> Drop for Odb<'repo> { impl<'repo> Odb<'repo> { /// Creates an object database without any backends. + /// + ///
+ /// + /// # SHA1-only limitation + /// + /// This method **always** creates a SHA1 object database. + /// + /// In future releases, this method will be removed entirely. + /// + ///
+ #[cfg(not(feature = "unstable-sha256"))] pub fn new<'a>() -> Result, Error> { crate::init(); unsafe { @@ -54,6 +65,25 @@ impl<'repo> Odb<'repo> { } } + /// Creates an object database without any backends, with the specified object format. + /// + /// The `format` parameter determines the OID type (SHA1 or SHA256) used by this object database. + /// + /// Note: Most users should obtain an ODB from [`Repository::odb`](crate::Repository::odb), + /// which automatically inherits the repository's object format. + #[cfg(feature = "unstable-sha256")] + pub fn with_object_format<'a>(format: crate::ObjectFormat) -> Result, Error> { + crate::init(); + unsafe { + let mut out = ptr::null_mut(); + let mut opts: raw::git_odb_options = std::mem::zeroed(); + opts.version = raw::GIT_ODB_OPTIONS_VERSION; + opts.oid_type = format.raw(); + try_call!(raw::git_odb_new(&mut out, &opts)); + Ok(Odb::from_raw(out)) + } + } + /// Create object database reading stream. /// /// Note that most backends do not support streaming reads because they store their objects as compressed/delta'ed blobs. @@ -144,9 +174,7 @@ impl<'repo> Odb<'repo> { /// Write an object to the database. pub fn write(&self, kind: ObjectType, data: &[u8]) -> Result { unsafe { - let mut out = raw::git_oid { - id: [0; raw::GIT_OID_MAX_SIZE], - }; + let mut out = crate::util::zeroed_raw_oid(); try_call!(raw::git_odb_write( &mut out, self.raw, @@ -194,9 +222,7 @@ impl<'repo> Odb<'repo> { /// Potentially finds an object that starts with the given prefix. pub fn exists_prefix(&self, short_oid: Oid, len: usize) -> Result { unsafe { - let mut out = raw::git_oid { - id: [0; raw::GIT_OID_MAX_SIZE], - }; + let mut out = crate::util::zeroed_raw_oid(); try_call!(raw::git_odb_exists_prefix( &mut out, self.raw, @@ -243,7 +269,10 @@ impl<'repo> Odb<'repo> { /// ```compile_fail /// use git2::Odb; /// let mempack = { + /// #[cfg(not(feature = "unstable-sha256"))] /// let odb = Odb::new().unwrap(); + /// #[cfg(feature = "unstable-sha256")] + /// let odb = Odb::with_object_format(git2::ObjectFormat::Sha1).unwrap(); /// odb.add_new_mempack_backend(1000).unwrap() /// }; /// ``` @@ -388,9 +417,7 @@ impl<'repo> OdbWriter<'repo> { /// This method will fail if the total number of received bytes differs from the size declared with odb_writer() /// Attempting write after finishing will be ignored. pub fn finalize(&mut self) -> Result { - let mut raw = raw::git_oid { - id: [0; raw::GIT_OID_MAX_SIZE], - }; + let mut raw = crate::util::zeroed_raw_oid(); unsafe { try_call!(raw::git_odb_stream_finalize_write(&mut raw, self.raw)); Ok(Binding::from_raw(&raw as *const _)) @@ -641,7 +668,13 @@ mod tests { let db = repo.odb().unwrap(); let id = db.write(ObjectType::Blob, &dat).unwrap(); let id_prefix_str = &id.to_string()[0..10]; - let id_prefix = Oid::from_str(id_prefix_str).unwrap(); + let id_prefix = { + #[cfg(not(feature = "unstable-sha256"))] + let oid = Oid::from_str(id_prefix_str).unwrap(); + #[cfg(feature = "unstable-sha256")] + let oid = Oid::from_str(id_prefix_str, repo.object_format()).unwrap(); + oid + }; let found_oid = db.exists_prefix(id_prefix, 10).unwrap(); assert_eq!(found_oid, id); } diff --git a/src/oid.rs b/src/oid.rs index 67798baa41..35687d1d2b 100644 --- a/src/oid.rs +++ b/src/oid.rs @@ -8,6 +8,37 @@ use crate::{raw, Error, IntoCString, ObjectType}; use crate::util::{c_cmp_to_ordering, Binding}; +/// Object ID format (hash algorithm). +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum ObjectFormat { + /// SHA1 object format (20-byte object IDs) + Sha1, + /// SHA256 object format (32-byte object IDs) + #[cfg(feature = "unstable-sha256")] + Sha256, +} + +impl Binding for ObjectFormat { + type Raw = raw::git_oid_t; + + unsafe fn from_raw(raw: raw::git_oid_t) -> Self { + match raw { + raw::GIT_OID_SHA1 => ObjectFormat::Sha1, + #[cfg(feature = "unstable-sha256")] + raw::GIT_OID_SHA256 => ObjectFormat::Sha256, + _ => panic!("Unknown git oid type"), + } + } + + fn raw(&self) -> Self::Raw { + match self { + ObjectFormat::Sha1 => raw::GIT_OID_SHA1, + #[cfg(feature = "unstable-sha256")] + ObjectFormat::Sha256 => raw::GIT_OID_SHA256, + } + } +} + /// Unique identity of any object (commit, tree, blob, tag). #[derive(Copy, Clone)] #[repr(C)] @@ -21,63 +52,99 @@ impl Oid { /// # Errors /// /// Returns an error if the string is empty, is longer than 40 hex - /// characters, or contains any non-hex characters. - pub fn from_str(s: &str) -> Result { + /// characters (or 64 for SHA256), or contains any non-hex characters. + pub fn from_str( + s: &str, + #[cfg(feature = "unstable-sha256")] format: crate::ObjectFormat, + ) -> Result { crate::init(); - let mut raw = raw::git_oid { - id: [0; raw::GIT_OID_MAX_SIZE], - }; + let mut raw = crate::util::zeroed_raw_oid(); + let data = s.as_bytes().as_ptr() as *const libc::c_char; + let len = s.len() as libc::size_t; unsafe { - try_call!(raw::git_oid_fromstrn( - &mut raw, - s.as_bytes().as_ptr() as *const libc::c_char, - s.len() as libc::size_t - )); + #[cfg(not(feature = "unstable-sha256"))] + try_call!(raw::git_oid_fromstrn(&mut raw, data, len)); + #[cfg(feature = "unstable-sha256")] + try_call!(raw::git_oid_fromstrn(&mut raw, data, len, format.raw())); } Ok(Oid { raw }) } /// Parse a raw object id into an Oid structure. /// - /// If the array given is not 20 bytes in length, an error is returned. + /// When the `unstable-sha256` feature is enabled, this automatically detects + /// the OID type based on byte length: + /// + /// - 20-byte arrays are parsed as SHA1 + /// - 32-byte arrays are parsed as SHA256 + /// + /// Without the feature, only 20-byte SHA1 OIDs are supported. + /// + /// # Errors + /// + /// Returns an error if the byte array is not a valid OID length. pub fn from_bytes(bytes: &[u8]) -> Result { crate::init(); - let mut raw = raw::git_oid { - id: [0; raw::GIT_OID_MAX_SIZE], - }; - if bytes.len() != raw::GIT_OID_MAX_SIZE { - Err(Error::from_str("raw byte array must be 20 bytes")) - } else { + let mut raw = crate::util::zeroed_raw_oid(); + + #[cfg(not(feature = "unstable-sha256"))] + { + if bytes.len() != raw::GIT_OID_SHA1_SIZE { + return Err(Error::from_str("raw byte array must be 20 bytes")); + } unsafe { try_call!(raw::git_oid_fromraw(&mut raw, bytes.as_ptr())); } - Ok(Oid { raw }) } + + #[cfg(feature = "unstable-sha256")] + { + let oid_type = match bytes.len() { + raw::GIT_OID_SHA1_SIZE => raw::GIT_OID_SHA1, + raw::GIT_OID_SHA256_SIZE => raw::GIT_OID_SHA256, + _ => { + return Err(Error::from_str( + "raw byte array must be 20 bytes (SHA1) or 32 bytes (SHA256)", + )) + } + }; + unsafe { + try_call!(raw::git_oid_fromraw(&mut raw, bytes.as_ptr(), oid_type)); + } + } + + Ok(Oid { raw }) } /// Creates an all zero Oid structure. pub fn zero() -> Oid { - let out = raw::git_oid { - id: [0; raw::GIT_OID_MAX_SIZE], - }; - Oid { raw: out } + Oid { + raw: crate::util::zeroed_raw_oid(), + } } /// Hashes the provided data as an object of the provided type, and returns /// an Oid corresponding to the result. This does not store the object /// inside any object database or repository. - pub fn hash_object(kind: ObjectType, bytes: &[u8]) -> Result { + pub fn hash_object( + kind: ObjectType, + bytes: &[u8], + #[cfg(feature = "unstable-sha256")] format: crate::ObjectFormat, + ) -> Result { crate::init(); - let mut out = raw::git_oid { - id: [0; raw::GIT_OID_MAX_SIZE], - }; + let mut out = crate::util::zeroed_raw_oid(); + let data = bytes.as_ptr() as *const libc::c_void; unsafe { + #[cfg(not(feature = "unstable-sha256"))] + try_call!(raw::git_odb_hash(&mut out, data, bytes.len(), kind.raw())); + #[cfg(feature = "unstable-sha256")] try_call!(raw::git_odb_hash( &mut out, - bytes.as_ptr() as *const libc::c_void, + data, bytes.len(), - kind.raw() + kind.raw(), + format.raw() )); } @@ -87,30 +154,43 @@ impl Oid { /// Hashes the content of the provided file as an object of the provided type, /// and returns an Oid corresponding to the result. This does not store the object /// inside any object database or repository. - pub fn hash_file>(kind: ObjectType, path: P) -> Result { + pub fn hash_file>( + kind: ObjectType, + path: P, + #[cfg(feature = "unstable-sha256")] format: crate::ObjectFormat, + ) -> Result { crate::init(); // Normal file path OK (does not need Windows conversion). let rpath = path.as_ref().into_c_string()?; - let mut out = raw::git_oid { - id: [0; raw::GIT_OID_MAX_SIZE], - }; + let mut out = crate::util::zeroed_raw_oid(); unsafe { + #[cfg(not(feature = "unstable-sha256"))] try_call!(raw::git_odb_hashfile(&mut out, rpath, kind.raw())); + #[cfg(feature = "unstable-sha256")] + try_call!(raw::git_odb_hashfile( + &mut out, + rpath, + kind.raw(), + format.raw() + )); } Ok(Oid { raw: out }) } - /// View this OID as a byte-slice 20 bytes in length. + /// View this OID as a byte-slice. + /// + /// * 20 bytes in length if the feature `unstable-sha256` is not enabled. + /// * 32 bytes in length if the feature `unstable-sha256` is enabled. pub fn as_bytes(&self) -> &[u8] { &self.raw.id } /// Test if this OID is all zeros. pub fn is_zero(&self) -> bool { - unsafe { raw::git_oid_iszero(&self.raw) == 1 } + unsafe { raw::git_oid_is_zero(&self.raw) == 1 } } } @@ -147,6 +227,7 @@ impl fmt::Display for Oid { } } +#[cfg(not(feature = "unstable-sha256"))] impl str::FromStr for Oid { type Err = Error; @@ -156,6 +237,25 @@ impl str::FromStr for Oid { /// /// Returns an error if the string is empty, is longer than 40 hex /// characters, or contains any non-hex characters. + /// + ///
+ /// + /// # SHA1-only limitation + /// + /// This method **always** parses as SHA1 (up to 40 hex characters). + /// It cannot parse SHA256 OIDs because [`str::FromStr::from_str`] lacks + /// the object format parameter. + /// + /// In future releases, this will be removed entirely to avoid misuse. + /// + /// Consider these alternatives: + /// + /// * [`Oid::from_str`] with explicit [`ObjectFormat`](crate::ObjectFormat) + /// * [`Oid::from_bytes`] if you have access to the underlying byte of the OID + /// * [`Repository::revparse_single`](crate::Repository::revparse_single) + /// if you have repository context + /// + ///
fn from_str(s: &str) -> Result { Oid::from_str(s) } @@ -197,12 +297,15 @@ mod tests { use std::fs::File; use std::io::prelude::*; + use libgit2_sys as raw; + use super::Error; use super::Oid; use crate::ObjectType; use tempfile::TempDir; #[test] + #[cfg(not(feature = "unstable-sha256"))] fn conversions() { assert!(Oid::from_str("foo").is_err()); assert!(Oid::from_str("decbf2be529ab6557d5429922251e5ee36519817").is_ok()); @@ -211,6 +314,46 @@ mod tests { } #[test] + #[cfg(feature = "unstable-sha256")] + fn conversions_object_format() { + use crate::ObjectFormat; + + assert!(Oid::from_str("foo", ObjectFormat::Sha1).is_err()); + assert!(Oid::from_str( + "decbf2be529ab6557d5429922251e5ee36519817", + ObjectFormat::Sha1 + ) + .is_ok()); + + assert!(Oid::from_str("foo", ObjectFormat::Sha256).is_err()); + assert!(Oid::from_str( + "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", + ObjectFormat::Sha256 + ) + .is_ok()); + + assert!(Oid::from_bytes(b"foo").is_err()); + + let sha1_from_bytes = Oid::from_bytes(&[0u8; 20]).unwrap(); + let sha256_from_bytes = Oid::from_bytes(&[0u8; 32]).unwrap(); + + // Both stored in 32-byte arrays when sha256 feature is enabled + assert_eq!(sha1_from_bytes.as_bytes().len(), raw::GIT_OID_MAX_SIZE); + assert_eq!(sha256_from_bytes.as_bytes().len(), raw::GIT_OID_MAX_SIZE); + + // Hex string output should differ based on OID type + assert_eq!(sha1_from_bytes.to_string().len(), raw::GIT_OID_SHA1_HEXSIZE); + assert_eq!( + sha256_from_bytes.to_string().len(), + raw::GIT_OID_SHA256_HEXSIZE + ); + + // Verify they're not equal despite being all zeros + assert_ne!(sha1_from_bytes, sha256_from_bytes); + } + + #[test] + #[cfg(not(feature = "unstable-sha256"))] fn comparisons() -> Result<(), Error> { assert_eq!(Oid::from_str("decbf2b")?, Oid::from_str("decbf2b")?); assert!(Oid::from_str("decbf2b")? <= Oid::from_str("decbf2b")?); @@ -229,10 +372,110 @@ mod tests { Oid::from_bytes(b"00000000000000000000")? < Oid::from_bytes(b"00000000000000000001")? ); assert!(Oid::from_bytes(b"00000000000000000000")? < Oid::from_str("decbf2b")?); + + Ok(()) + } + + #[test] + #[cfg(feature = "unstable-sha256")] + fn comparisons_object_format() -> Result<(), Error> { + use crate::ObjectFormat; + + // SHA1 OID comparisons with explicit format assert_eq!( - Oid::from_bytes(b"00000000000000000000")?, - Oid::from_str("3030303030303030303030303030303030303030")? + Oid::from_str("decbf2b", ObjectFormat::Sha1)?, + Oid::from_str("decbf2b", ObjectFormat::Sha1)? ); + assert!( + Oid::from_str("decbf2b", ObjectFormat::Sha1)? + <= Oid::from_str("decbf2b", ObjectFormat::Sha1)? + ); + assert!( + Oid::from_str("decbf2b", ObjectFormat::Sha1)? + >= Oid::from_str("decbf2b", ObjectFormat::Sha1)? + ); + { + let o = Oid::from_str("decbf2b", ObjectFormat::Sha1)?; + assert_eq!(o, o); + assert!(o <= o); + assert!(o >= o); + } + assert_eq!( + Oid::from_str("decbf2b", ObjectFormat::Sha1)?, + Oid::from_str( + "decbf2b000000000000000000000000000000000", + ObjectFormat::Sha1 + )? + ); + + // SHA1 byte comparisons (20 bytes) + assert!( + Oid::from_bytes(b"00000000000000000000")? < Oid::from_bytes(b"00000000000000000001")? + ); + assert!( + Oid::from_bytes(b"00000000000000000000")? + < Oid::from_str("decbf2b", ObjectFormat::Sha1)? + ); + + // SHA256 OID comparisons with explicit format (using full 64-char hex strings) + assert_eq!( + Oid::from_str( + "abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890", + ObjectFormat::Sha256 + )?, + Oid::from_str( + "abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890", + ObjectFormat::Sha256 + )? + ); + assert!( + Oid::from_str( + "abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890", + ObjectFormat::Sha256 + )? <= Oid::from_str( + "abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890", + ObjectFormat::Sha256 + )? + ); + assert!( + Oid::from_str( + "abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890", + ObjectFormat::Sha256 + )? >= Oid::from_str( + "abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890", + ObjectFormat::Sha256 + )? + ); + { + let o = Oid::from_str( + "abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890", + ObjectFormat::Sha256, + )?; + assert_eq!(o, o); + assert!(o <= o); + assert!(o >= o); + } + assert_eq!( + Oid::from_str("abcdef12", ObjectFormat::Sha256)?, + Oid::from_str( + "abcdef1200000000000000000000000000000000000000000000000000000000", + ObjectFormat::Sha256 + )? + ); + + // SHA256 byte comparisons (32 bytes) + assert!( + Oid::from_bytes(b"00000000000000000000000000000000")? + < Oid::from_bytes(b"00000000000000000000000000000001")? + ); + assert!( + Oid::from_bytes(b"00000000000000000000000000000000")? + < Oid::from_str( + "abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890", + ObjectFormat::Sha256 + )? + ); + Ok(()) } @@ -242,17 +485,68 @@ mod tests { } #[test] + #[cfg(not(feature = "unstable-sha256"))] fn hash_object() { let bytes = "Hello".as_bytes(); - assert!(Oid::hash_object(ObjectType::Blob, bytes).is_ok()); + let oid = Oid::hash_object(ObjectType::Blob, bytes).unwrap(); + assert_eq!(oid.to_string().len(), raw::GIT_OID_SHA1_HEXSIZE); + assert_eq!(oid.as_bytes().len(), raw::GIT_OID_MAX_SIZE); + } + + #[test] + #[cfg(feature = "unstable-sha256")] + fn hash_object_with_format() -> Result<(), Error> { + use crate::ObjectFormat; + + let bytes = b"hello world"; + + let sha1_oid = Oid::hash_object(ObjectType::Blob, bytes, ObjectFormat::Sha1)?; + assert_eq!(sha1_oid.to_string().len(), raw::GIT_OID_SHA1_HEXSIZE); + assert_eq!(sha1_oid.as_bytes().len(), raw::GIT_OID_MAX_SIZE); + + let sha256_oid = Oid::hash_object(ObjectType::Blob, bytes, ObjectFormat::Sha256)?; + assert_eq!(sha256_oid.to_string().len(), raw::GIT_OID_SHA256_HEXSIZE); + assert_eq!(sha256_oid.as_bytes().len(), raw::GIT_OID_MAX_SIZE); + + // Different formats produce different OIDs + assert_ne!(sha1_oid, sha256_oid); + + Ok(()) } #[test] + #[cfg(not(feature = "unstable-sha256"))] fn hash_file() { let td = TempDir::new().unwrap(); let path = td.path().join("hello.txt"); let mut file = File::create(&path).unwrap(); file.write_all("Hello".as_bytes()).unwrap(); - assert!(Oid::hash_file(ObjectType::Blob, &path).is_ok()); + let oid = Oid::hash_file(ObjectType::Blob, &path).unwrap(); + assert_eq!(oid.to_string().len(), raw::GIT_OID_SHA1_HEXSIZE); + assert_eq!(oid.as_bytes().len(), raw::GIT_OID_MAX_SIZE); + } + + #[test] + #[cfg(feature = "unstable-sha256")] + fn hash_file_object_format() -> Result<(), Error> { + use crate::ObjectFormat; + + let td = TempDir::new().unwrap(); + let path = td.path().join("test.txt"); + let mut file = File::create(&path).unwrap(); + file.write_all(b"test content").unwrap(); + + let sha1_oid = Oid::hash_object(ObjectType::Blob, b"test content", ObjectFormat::Sha1)?; + assert_eq!(sha1_oid.to_string().len(), raw::GIT_OID_SHA1_HEXSIZE); + assert_eq!(sha1_oid.as_bytes().len(), raw::GIT_OID_MAX_SIZE); + + let sha256_oid = Oid::hash_object(ObjectType::Blob, b"test content", ObjectFormat::Sha256)?; + assert_eq!(sha256_oid.to_string().len(), raw::GIT_OID_SHA256_HEXSIZE); + assert_eq!(sha256_oid.as_bytes().len(), raw::GIT_OID_MAX_SIZE); + + // Different formats produce different OIDs + assert_ne!(sha1_oid, sha256_oid); + + Ok(()) } } diff --git a/src/packbuilder.rs b/src/packbuilder.rs index de47bbce32..f221e793f6 100644 --- a/src/packbuilder.rs +++ b/src/packbuilder.rs @@ -293,10 +293,7 @@ extern "C" fn progress_c( #[cfg(test)] mod tests { - use crate::{Buf, Oid}; - - // hash of a packfile constructed without any objects in it - const EMPTY_PACKFILE_OID: &str = "029d08823bd8a8eab510ad6ac75c823cfd3ed31e"; + use crate::Buf; fn pack_header(len: u8) -> Vec { [].iter() @@ -314,8 +311,25 @@ mod tests { 0x02, 0x9d, 0x08, 0x82, 0x3b, // ^ 0xd8, 0xa8, 0xea, 0xb5, 0x10, // | SHA-1 of the zero 0xad, 0x6a, 0xc7, 0x5c, 0x82, // | object pack header - 0x3c, 0xfd, 0x3e, 0xd3, 0x1e, - ]) // v + 0x3c, 0xfd, 0x3e, 0xd3, 0x1e, // v + ]) + .cloned() + .collect::>() + } + + #[cfg(feature = "unstable-sha256")] + fn empty_pack_header_sha256() -> Vec { + pack_header(0) + .iter() + .chain(&[ + 0x7e, 0xd8, 0x90, 0xd8, 0xa4, // ^ + 0x57, 0x60, 0xf3, 0xee, 0xcf, // | SHA-256 of the zero + 0x73, 0x04, 0x5b, 0x1d, 0x10, // | object pack header + 0x47, 0x08, 0x5a, 0xf4, 0x77, // | + 0x6d, 0xc6, 0x83, 0xd7, 0x8e, // | + 0xac, 0x82, 0x20, 0x3d, 0xf1, // | + 0x99, 0x3f, // v + ]) .cloned() .collect::>() } @@ -340,16 +354,46 @@ mod tests { assert_eq!(&*buf, &*empty_pack_header()); } + #[test] + #[cfg(feature = "unstable-sha256")] + fn smoke_write_buf_sha256() { + let (_td, repo) = crate::test::repo_init_sha256(); + let mut builder = t!(repo.packbuilder()); + let mut buf = Buf::new(); + t!(builder.write_buf(&mut buf)); + assert!(builder.name().is_none()); + assert_eq!(&*buf, &*empty_pack_header_sha256()); + } + #[test] fn smoke_write() { + // SHA1 hash of a packfile constructed without any objects in it + const EMPTY_PACKFILE_OID: &str = "029d08823bd8a8eab510ad6ac75c823cfd3ed31e"; + let (_td, repo) = crate::test::repo_init(); let mut builder = t!(repo.packbuilder()); t!(builder.write(repo.path(), 0)); + #[cfg(not(feature = "unstable-sha256"))] #[allow(deprecated)] { - assert!(builder.hash().unwrap() == Oid::from_str(EMPTY_PACKFILE_OID).unwrap()); + let oid = crate::Oid::from_str(EMPTY_PACKFILE_OID).unwrap(); + assert_eq!(builder.hash().unwrap(), oid); } - assert!(builder.name().unwrap() == EMPTY_PACKFILE_OID); + assert_eq!(builder.name().unwrap(), EMPTY_PACKFILE_OID); + } + + #[test] + #[cfg(feature = "unstable-sha256")] + #[ignore = "git_packbuilder_write hasnt yet supported sha256; see https://github.com/libgit2/libgit2/pull/7179"] + fn smoke_write_object_format() { + // SHA256 hash of a packfile constructed without any objects in it + const EMPTY_PACKFILE_OID_SHA256: &str = + "7ed890d8a45760f3eecf73045b1d1047085af4776dc683d78eac82203df1993f"; + + let (_td, repo) = crate::test::repo_init_sha256(); + let mut builder = t!(repo.packbuilder()); + t!(builder.write(repo.path(), 0)); + assert_eq!(builder.name().unwrap(), EMPTY_PACKFILE_OID_SHA256); } #[test] @@ -364,6 +408,19 @@ mod tests { assert_eq!(&*buf, &*empty_pack_header()); } + #[test] + #[cfg(feature = "unstable-sha256")] + fn smoke_foreach_sha256() { + let (_td, repo) = crate::test::repo_init_sha256(); + let mut builder = t!(repo.packbuilder()); + let mut buf = Vec::::new(); + t!(builder.foreach(|bytes| { + buf.extend(bytes); + true + })); + assert_eq!(&*buf, &*empty_pack_header_sha256()); + } + #[test] fn insert_write_buf() { let (_td, repo) = crate::test::repo_init(); @@ -377,6 +434,20 @@ mod tests { assert_eq!(&buf[0..12], &*pack_header(1)); } + #[test] + #[cfg(feature = "unstable-sha256")] + fn insert_write_buf_sha256() { + let (_td, repo) = crate::test::repo_init_sha256(); + let mut builder = t!(repo.packbuilder()); + let mut buf = Buf::new(); + let (commit, _tree) = crate::test::commit(&repo); + t!(builder.insert_object(commit, None)); + assert_eq!(builder.object_count(), 1); + t!(builder.write_buf(&mut buf)); + // Just check that the correct number of objects are written + assert_eq!(&buf[0..12], &*pack_header(1)); + } + #[test] fn insert_tree_write_buf() { let (_td, repo) = crate::test::repo_init(); @@ -391,6 +462,21 @@ mod tests { assert_eq!(&buf[0..12], &*pack_header(2)); } + #[test] + #[cfg(feature = "unstable-sha256")] + fn insert_tree_write_buf_sha256() { + let (_td, repo) = crate::test::repo_init_sha256(); + let mut builder = t!(repo.packbuilder()); + let mut buf = Buf::new(); + let (_commit, tree) = crate::test::commit(&repo); + // will insert the tree itself and the blob, 2 objects + t!(builder.insert_tree(tree)); + assert_eq!(builder.object_count(), 2); + t!(builder.write_buf(&mut buf)); + // Just check that the correct number of objects are written + assert_eq!(&buf[0..12], &*pack_header(2)); + } + #[test] fn insert_commit_write_buf() { let (_td, repo) = crate::test::repo_init(); @@ -405,6 +491,21 @@ mod tests { assert_eq!(&buf[0..12], &*pack_header(3)); } + #[test] + #[cfg(feature = "unstable-sha256")] + fn insert_commit_write_buf_sha256() { + let (_td, repo) = crate::test::repo_init_sha256(); + let mut builder = t!(repo.packbuilder()); + let mut buf = Buf::new(); + let (commit, _tree) = crate::test::commit(&repo); + // will insert the commit, its tree and the blob, 3 objects + t!(builder.insert_commit(commit)); + assert_eq!(builder.object_count(), 3); + t!(builder.write_buf(&mut buf)); + // Just check that the correct number of objects are written + assert_eq!(&buf[0..12], &*pack_header(3)); + } + #[test] fn insert_write() { let (_td, repo) = crate::test::repo_init(); @@ -416,6 +517,19 @@ mod tests { t!(repo.find_commit(commit)); } + #[test] + #[cfg(feature = "unstable-sha256")] + #[ignore = "git_packbuilder_write hasnt yet supported sha256; see https://github.com/libgit2/libgit2/pull/7179"] + fn insert_write_sha256() { + let (_td, repo) = crate::test::repo_init_sha256(); + let mut builder = t!(repo.packbuilder()); + let (commit, _tree) = crate::test::commit(&repo); + t!(builder.insert_object(commit, None)); + assert_eq!(builder.object_count(), 1); + t!(builder.write(repo.path(), 0)); + t!(repo.find_commit(commit)); + } + #[test] fn insert_tree_write() { let (_td, repo) = crate::test::repo_init(); @@ -428,6 +542,20 @@ mod tests { t!(repo.find_tree(tree)); } + #[test] + #[cfg(feature = "unstable-sha256")] + #[ignore = "git_packbuilder_write hasnt yet supported sha256; see https://github.com/libgit2/libgit2/pull/7179"] + fn insert_tree_write_sha256() { + let (_td, repo) = crate::test::repo_init_sha256(); + let mut builder = t!(repo.packbuilder()); + let (_commit, tree) = crate::test::commit(&repo); + // will insert the tree itself and the blob, 2 objects + t!(builder.insert_tree(tree)); + assert_eq!(builder.object_count(), 2); + t!(builder.write(repo.path(), 0)); + t!(repo.find_tree(tree)); + } + #[test] fn insert_commit_write() { let (_td, repo) = crate::test::repo_init(); @@ -440,6 +568,20 @@ mod tests { t!(repo.find_commit(commit)); } + #[test] + #[cfg(feature = "unstable-sha256")] + #[ignore = "git_packbuilder_write hasnt yet supported sha256; see https://github.com/libgit2/libgit2/pull/7179"] + fn insert_commit_write_sha256() { + let (_td, repo) = crate::test::repo_init_sha256(); + let mut builder = t!(repo.packbuilder()); + let (commit, _tree) = crate::test::commit(&repo); + // will insert the commit, its tree and the blob, 3 objects + t!(builder.insert_commit(commit)); + assert_eq!(builder.object_count(), 3); + t!(builder.write(repo.path(), 0)); + t!(repo.find_commit(commit)); + } + #[test] fn progress_callback() { let mut progress_called = false; diff --git a/src/repo.rs b/src/repo.rs index 5b7594227e..7b67bd7335 100644 --- a/src/repo.rs +++ b/src/repo.rs @@ -17,6 +17,7 @@ use crate::tagforeach::{tag_foreach_cb, TagForeachCB, TagForeachData}; use crate::util::{self, path_to_repo_path, Binding}; use crate::worktree::{Worktree, WorktreeAddOptions}; use crate::CherrypickOptions; +use crate::ObjectFormat; use crate::RevertOptions; use crate::{mailmap::Mailmap, panic}; use crate::{ @@ -123,9 +124,17 @@ pub struct RepositoryInitOptions { template_path: Option, initial_head: Option, origin_url: Option, + #[cfg(feature = "unstable-sha256")] + oid_type: Option, } impl Repository { + /// Returns the object ID format (hash algorithm) used by this repository. + pub fn object_format(&self) -> ObjectFormat { + let oid_type = unsafe { raw::git_repository_oid_type(self.raw()) }; + unsafe { Binding::from_raw(oid_type) } + } + /// Attempt to open an already-existing repository at `path`. /// /// The path can point to either a normal or bare repository. @@ -1132,9 +1141,7 @@ impl Repository { /// The Oid returned can in turn be passed to `find_blob` to get a handle to /// the blob. pub fn blob(&self, data: &[u8]) -> Result { - let mut raw = raw::git_oid { - id: [0; raw::GIT_OID_MAX_SIZE], - }; + let mut raw = crate::util::zeroed_raw_oid(); unsafe { let ptr = data.as_ptr() as *const c_void; let len = data.len() as size_t; @@ -1156,9 +1163,7 @@ impl Repository { pub fn blob_path(&self, path: &Path) -> Result { // Normal file path OK (does not need Windows conversion). let path = path.into_c_string()?; - let mut raw = raw::git_oid { - id: [0; raw::GIT_OID_MAX_SIZE], - }; + let mut raw = crate::util::zeroed_raw_oid(); unsafe { try_call!(raw::git_blob_create_fromdisk(&mut raw, self.raw(), path)); Ok(Binding::from_raw(&raw as *const _)) @@ -1309,9 +1314,7 @@ impl Repository { .map(|p| p.raw() as *const raw::git_commit) .collect::>(); let message = CString::new(message)?; - let mut raw = raw::git_oid { - id: [0; raw::GIT_OID_MAX_SIZE], - }; + let mut raw = crate::util::zeroed_raw_oid(); unsafe { try_call!(raw::git_commit_create( &mut raw, @@ -1383,9 +1386,7 @@ impl Repository { let commit_content = CString::new(commit_content)?; let signature = CString::new(signature)?; let signature_field = crate::opt_cstr(signature_field)?; - let mut raw = raw::git_oid { - id: [0; raw::GIT_OID_MAX_SIZE], - }; + let mut raw = crate::util::zeroed_raw_oid(); unsafe { try_call!(raw::git_commit_create_with_signature( &mut raw, @@ -1438,7 +1439,13 @@ impl Repository { try_call!(raw::git_commit_lookup_prefix( &mut raw, self.raw(), - Oid::from_str(prefix_hash)?.raw(), + { + #[cfg(not(feature = "unstable-sha256"))] + let oid = Oid::from_str(prefix_hash)?; + #[cfg(feature = "unstable-sha256")] + let oid = Oid::from_str(prefix_hash, self.object_format())?; + oid.raw() + }, prefix_hash.len() )); Ok(Binding::from_raw(raw)) @@ -1483,7 +1490,13 @@ impl Repository { try_call!(raw::git_object_lookup_prefix( &mut raw, self.raw(), - Oid::from_str(prefix_hash)?.raw(), + { + #[cfg(not(feature = "unstable-sha256"))] + let oid = Oid::from_str(prefix_hash)?; + #[cfg(feature = "unstable-sha256")] + let oid = Oid::from_str(prefix_hash, self.object_format())?; + oid.raw() + }, prefix_hash.len(), kind )); @@ -1682,9 +1695,7 @@ impl Repository { /// allocate or free any `Reference` objects for simple situations. pub fn refname_to_id(&self, name: &str) -> Result { let name = CString::new(name)?; - let mut ret = raw::git_oid { - id: [0; raw::GIT_OID_MAX_SIZE], - }; + let mut ret = crate::util::zeroed_raw_oid(); unsafe { try_call!(raw::git_reference_name_to_id(&mut ret, self.raw(), name)); Ok(Binding::from_raw(&ret as *const _)) @@ -1910,9 +1921,7 @@ impl Repository { ) -> Result { let name = CString::new(name)?; let message = CString::new(message)?; - let mut raw = raw::git_oid { - id: [0; raw::GIT_OID_MAX_SIZE], - }; + let mut raw = crate::util::zeroed_raw_oid(); unsafe { try_call!(raw::git_tag_create( &mut raw, @@ -1943,9 +1952,7 @@ impl Repository { ) -> Result { let name = CString::new(name)?; let message = CString::new(message)?; - let mut raw_oid = raw::git_oid { - id: [0; raw::GIT_OID_MAX_SIZE], - }; + let mut raw_oid = crate::util::zeroed_raw_oid(); unsafe { try_call!(raw::git_tag_annotation_create( &mut raw_oid, @@ -1971,9 +1978,7 @@ impl Repository { force: bool, ) -> Result { let name = CString::new(name)?; - let mut raw = raw::git_oid { - id: [0; raw::GIT_OID_MAX_SIZE], - }; + let mut raw = crate::util::zeroed_raw_oid(); unsafe { try_call!(raw::git_tag_create_lightweight( &mut raw, @@ -2002,7 +2007,13 @@ impl Repository { try_call!(raw::git_tag_lookup_prefix( &mut raw, self.raw, - Oid::from_str(prefix_hash)?.raw(), + { + #[cfg(not(feature = "unstable-sha256"))] + let oid = Oid::from_str(prefix_hash)?; + #[cfg(feature = "unstable-sha256")] + let oid = Oid::from_str(prefix_hash, self.object_format())?; + oid.raw() + }, prefix_hash.len() )); Ok(Binding::from_raw(raw)) @@ -2347,9 +2358,7 @@ impl Repository { ) -> Result { let notes_ref = crate::opt_cstr(notes_ref)?; let note = CString::new(note)?; - let mut ret = raw::git_oid { - id: [0; raw::GIT_OID_MAX_SIZE], - }; + let mut ret = crate::util::zeroed_raw_oid(); unsafe { try_call!(raw::git_note_create( &mut ret, @@ -2463,9 +2472,7 @@ impl Repository { /// Find a merge base between two commits pub fn merge_base(&self, one: Oid, two: Oid) -> Result { - let mut raw = raw::git_oid { - id: [0; raw::GIT_OID_MAX_SIZE], - }; + let mut raw = crate::util::zeroed_raw_oid(); unsafe { try_call!(raw::git_merge_base( &mut raw, @@ -2511,9 +2518,7 @@ impl Repository { /// If you're looking to recieve the common merge base between all the /// given commits, use [`Self::merge_base_octopus`]. pub fn merge_base_many(&self, oids: &[Oid]) -> Result { - let mut raw = raw::git_oid { - id: [0; raw::GIT_OID_MAX_SIZE], - }; + let mut raw = crate::util::zeroed_raw_oid(); unsafe { try_call!(raw::git_merge_base_many( @@ -2528,9 +2533,7 @@ impl Repository { /// Find a common merge base between all given a list of commits pub fn merge_base_octopus(&self, oids: &[Oid]) -> Result { - let mut raw = raw::git_oid { - id: [0; raw::GIT_OID_MAX_SIZE], - }; + let mut raw = crate::util::zeroed_raw_oid(); unsafe { try_call!(raw::git_merge_base_octopus( @@ -2973,9 +2976,7 @@ impl Repository { flags: Option, ) -> Result { unsafe { - let mut raw_oid = raw::git_oid { - id: [0; raw::GIT_OID_MAX_SIZE], - }; + let mut raw_oid = crate::util::zeroed_raw_oid(); let message = crate::opt_cstr(message)?; let flags = flags.unwrap_or_else(StashFlags::empty); try_call!(raw::git_stash_save( @@ -2995,9 +2996,7 @@ impl Repository { opts: Option<&mut StashSaveOptions<'_>>, ) -> Result { unsafe { - let mut raw_oid = raw::git_oid { - id: [0; raw::GIT_OID_MAX_SIZE], - }; + let mut raw_oid = crate::util::zeroed_raw_oid(); let opts = opts.map(|opts| opts.raw()); try_call!(raw::git_stash_save_with_opts( &mut raw_oid, @@ -3410,6 +3409,8 @@ impl RepositoryInitOptions { template_path: None, initial_head: None, origin_url: None, + #[cfg(feature = "unstable-sha256")] + oid_type: None, } } @@ -3529,6 +3530,17 @@ impl RepositoryInitOptions { self } + /// Set the object format (hash algorithm) for the repository. + /// + /// The default is [`ObjectFormat::Sha1`]. + /// Setting this to [`ObjectFormat::Sha256`] + /// will create a repository that uses SHA256 object IDs. + #[cfg(feature = "unstable-sha256")] + pub fn object_format(&mut self, format: ObjectFormat) -> &mut RepositoryInitOptions { + self.oid_type = Some(format.raw()); + self + } + /// Creates a set of raw init options to be used with /// `git_repository_init_ext`. /// @@ -3550,6 +3562,10 @@ impl RepositoryInitOptions { opts.template_path = crate::call::convert(&self.template_path); opts.initial_head = crate::call::convert(&self.initial_head); opts.origin_url = crate::call::convert(&self.origin_url); + #[cfg(feature = "unstable-sha256")] + if let Some(oid_type) = self.oid_type { + opts.oid_type = oid_type; + } opts } } @@ -3557,13 +3573,20 @@ impl RepositoryInitOptions { #[cfg(test)] mod tests { use crate::build::CheckoutBuilder; + #[cfg(feature = "unstable-sha256")] + use crate::ObjectFormat; + #[cfg(feature = "unstable-sha256")] + use crate::RepositoryInitOptions; use crate::{CherrypickOptions, MergeFileOptions}; use crate::{ ObjectType, Oid, Repository, ResetType, Signature, SubmoduleIgnore, SubmoduleUpdate, }; + use std::ffi::OsStr; use std::fs; use std::path::Path; + + use libgit2_sys as raw; use tempfile::TempDir; #[test] @@ -3573,6 +3596,33 @@ mod tests { let repo = Repository::init(path).unwrap(); assert!(!repo.is_bare()); + + let oid = repo.blob(b"test content").unwrap(); + assert_eq!(oid.as_bytes().len(), raw::GIT_OID_MAX_SIZE); + // Verify OID hex string is 40 characters for SHA1 + assert_eq!(oid.to_string().len(), raw::GIT_OID_SHA1_HEXSIZE); + } + + #[test] + #[cfg(feature = "unstable-sha256")] + fn smoke_init_sha256() { + let td = TempDir::new().unwrap(); + let path = td.path(); + + let mut opts = RepositoryInitOptions::new(); + opts.object_format(ObjectFormat::Sha256); + + let repo = Repository::init_opts(path, &opts).unwrap(); + assert!(!repo.is_bare()); + assert_eq!(repo.object_format(), ObjectFormat::Sha256); + + let oid = repo.blob(b"test content").unwrap(); + assert_eq!(oid.as_bytes().len(), raw::GIT_OID_MAX_SIZE); + assert_eq!(oid.to_string().len(), raw::GIT_OID_SHA256_HEXSIZE); + + let config = repo.config().unwrap(); + let format = config.get_string("extensions.objectformat").unwrap(); + assert_eq!(format, "sha256"); } #[test] @@ -3585,6 +3635,22 @@ mod tests { assert!(repo.namespace().is_none()); } + #[test] + #[cfg(feature = "unstable-sha256")] + fn smoke_init_bare_sha256() { + let td = TempDir::new().unwrap(); + let path = td.path(); + + let mut opts = RepositoryInitOptions::new(); + opts.object_format(ObjectFormat::Sha256); + opts.bare(true); + + let repo = Repository::init_opts(path, &opts).unwrap(); + assert!(repo.is_bare()); + assert!(repo.namespace().is_none()); + assert_eq!(repo.object_format(), ObjectFormat::Sha256); + } + #[test] fn smoke_open() { let td = TempDir::new().unwrap(); @@ -3599,6 +3665,39 @@ mod tests { crate::test::realpath(&td.path().join(".git/")).unwrap() ); assert_eq!(repo.state(), crate::RepositoryState::Clean); + + let oid = repo.blob(b"test content").unwrap(); + assert_eq!(oid.as_bytes().len(), raw::GIT_OID_MAX_SIZE); + assert_eq!(oid.to_string().len(), raw::GIT_OID_SHA1_HEXSIZE); + } + + #[test] + #[cfg(feature = "unstable-sha256")] + fn smoke_open_sha256() { + let td = TempDir::new().unwrap(); + let path = td.path(); + + let mut opts = RepositoryInitOptions::new(); + opts.object_format(ObjectFormat::Sha256); + Repository::init_opts(path, &opts).unwrap(); + + let repo = Repository::open(path).unwrap(); + assert_eq!(repo.object_format(), ObjectFormat::Sha256); + assert!(!repo.is_bare()); + assert!(repo.is_empty().unwrap()); + assert_eq!( + crate::test::realpath(&repo.path()).unwrap(), + crate::test::realpath(&td.path().join(".git/")).unwrap() + ); + assert_eq!(repo.state(), crate::RepositoryState::Clean); + + let oid = repo.blob(b"test content").unwrap(); + assert_eq!(oid.as_bytes().len(), raw::GIT_OID_MAX_SIZE); + assert_eq!(oid.to_string().len(), raw::GIT_OID_SHA256_HEXSIZE); + + let config = repo.config().unwrap(); + let format = config.get_string("extensions.objectformat").unwrap(); + assert_eq!(format, "sha256"); } #[test] @@ -3615,12 +3714,43 @@ mod tests { ); } + #[test] + #[cfg(feature = "unstable-sha256")] + fn smoke_open_bare_sha256() { + let td = TempDir::new().unwrap(); + let path = td.path(); + + let mut opts = RepositoryInitOptions::new(); + opts.object_format(ObjectFormat::Sha256); + opts.bare(true); + + Repository::init_opts(path, &opts).unwrap(); + + let repo = Repository::open(path).unwrap(); + assert!(repo.is_bare()); + assert_eq!( + crate::test::realpath(&repo.path()).unwrap(), + crate::test::realpath(&td.path().join("")).unwrap() + ); + } + #[test] fn smoke_checkout() { let (_td, repo) = crate::test::repo_init(); repo.checkout_head(None).unwrap(); } + #[test] + #[cfg(feature = "unstable-sha256")] + fn smoke_checkout_sha256() { + let (_td, repo) = crate::test::repo_init_sha256(); + repo.checkout_head(None).unwrap(); + + let config = repo.config().unwrap(); + let format = config.get_string("extensions.objectformat").unwrap(); + assert_eq!(format, "sha256"); + } + #[test] fn smoke_revparse() { let (_td, repo) = crate::test::repo_init(); @@ -3638,6 +3768,28 @@ mod tests { t!(repo.reset(&obj, ResetType::Soft, Some(&mut opts))); } + #[test] + #[cfg(feature = "unstable-sha256")] + fn smoke_revparse_sha256() { + let (_td, repo) = crate::test::repo_init_sha256(); + let rev = repo.revparse("HEAD").unwrap(); + assert!(rev.to().is_none()); + let from = rev.from().unwrap(); + assert!(rev.from().is_some()); + + assert_eq!(repo.revparse_single("HEAD").unwrap().id(), from.id()); + let obj = repo.find_object(from.id(), None).unwrap().clone(); + obj.peel(ObjectType::Any).unwrap(); + obj.short_id().unwrap(); + repo.reset(&obj, ResetType::Hard, None).unwrap(); + let mut opts = CheckoutBuilder::new(); + t!(repo.reset(&obj, ResetType::Soft, Some(&mut opts))); + + let config = repo.config().unwrap(); + let format = config.get_string("extensions.objectformat").unwrap(); + assert_eq!(format, "sha256"); + } + #[test] fn makes_dirs() { let td = TempDir::new().unwrap(); @@ -3816,7 +3968,10 @@ mod tests { fn smoke_set_head_detached() { let (_td, repo) = crate::test::repo_init(); + #[cfg(not(feature = "unstable-sha256"))] let void_oid = Oid::from_bytes(b"00000000000000000000").unwrap(); + #[cfg(feature = "unstable-sha256")] + let void_oid = Oid::from_bytes(b"00000000000000000000000000000000").unwrap(); assert!(repo.set_head_detached(void_oid).is_err()); let main_oid = repo.revparse_single("main").unwrap().id(); diff --git a/src/revwalk.rs b/src/revwalk.rs index 3bb1e82b33..3e1d221c31 100644 --- a/src/revwalk.rs +++ b/src/revwalk.rs @@ -238,9 +238,7 @@ impl<'repo> Drop for Revwalk<'repo> { impl<'repo> Iterator for Revwalk<'repo> { type Item = Result; fn next(&mut self) -> Option> { - let mut out: raw::git_oid = raw::git_oid { - id: [0; raw::GIT_OID_MAX_SIZE], - }; + let mut out: raw::git_oid = crate::util::zeroed_raw_oid(); unsafe { try_call_iter!(raw::git_revwalk_next(&mut out, self.raw())); Some(Ok(Binding::from_raw(&out as *const _))) diff --git a/src/test.rs b/src/test.rs index 57a590f519..b9ec23fb93 100644 --- a/src/test.rs +++ b/src/test.rs @@ -37,6 +37,28 @@ pub fn repo_init() -> (TempDir, Repository) { (td, repo) } +#[cfg(feature = "unstable-sha256")] +pub fn repo_init_sha256() -> (TempDir, Repository) { + let td = TempDir::new().unwrap(); + let mut opts = RepositoryInitOptions::new(); + opts.initial_head("main"); + opts.object_format(crate::ObjectFormat::Sha256); + let repo = Repository::init_opts(td.path(), &opts).unwrap(); + { + let mut config = repo.config().unwrap(); + config.set_str("user.name", "name").unwrap(); + config.set_str("user.email", "email").unwrap(); + let mut index = repo.index().unwrap(); + let id = index.write_tree().unwrap(); + + let tree = repo.find_tree(id).unwrap(); + let sig = repo.signature().unwrap(); + repo.commit(Some("HEAD"), &sig, &sig, "initial\n\nbody", &tree, &[]) + .unwrap(); + } + (td, repo) +} + pub fn commit(repo: &Repository) -> (Oid, Oid) { let mut index = t!(repo.index()); let root = repo.path().parent().unwrap(); diff --git a/src/transaction.rs b/src/transaction.rs index 4f661f1d48..84caeadee4 100644 --- a/src/transaction.rs +++ b/src/transaction.rs @@ -162,7 +162,8 @@ mod tests { t!(tx.lock_ref("refs/heads/main")); t!(tx.lock_ref("refs/heads/next")); - t!(tx.set_target("refs/heads/main", Oid::zero(), None, "set main to zero")); + let oid = Oid::from_bytes(&[1u8; 20]).unwrap(); + t!(tx.set_target("refs/heads/main", oid, None, "set main to all ones")); t!(tx.set_symbolic_target( "refs/heads/next", "refs/heads/main", @@ -172,7 +173,38 @@ mod tests { t!(tx.commit()); - assert_eq!(repo.refname_to_id("refs/heads/main").unwrap(), Oid::zero()); + assert_eq!(repo.refname_to_id("refs/heads/main").unwrap(), oid); + assert_eq!( + repo.find_reference("refs/heads/next") + .unwrap() + .symbolic_target() + .unwrap(), + "refs/heads/main" + ); + } + + #[test] + #[cfg(feature = "unstable-sha256")] + fn smoke_sha256() { + let (_td, repo) = crate::test::repo_init_sha256(); + + let mut tx = t!(repo.transaction()); + + t!(tx.lock_ref("refs/heads/main")); + t!(tx.lock_ref("refs/heads/next")); + + let oid = Oid::from_bytes(&[1u8; 32]).unwrap(); + t!(tx.set_target("refs/heads/main", oid, None, "set main to all ones")); + t!(tx.set_symbolic_target( + "refs/heads/next", + "refs/heads/main", + None, + "set next to main", + )); + + t!(tx.commit()); + + assert_eq!(repo.refname_to_id("refs/heads/main").unwrap(), oid); assert_eq!( repo.find_reference("refs/heads/next") .unwrap() diff --git a/src/treebuilder.rs b/src/treebuilder.rs index 29ad54bb3b..eba7f45a9c 100644 --- a/src/treebuilder.rs +++ b/src/treebuilder.rs @@ -114,9 +114,7 @@ impl<'repo> TreeBuilder<'repo> { /// Write the contents of the TreeBuilder as a Tree object and /// return its Oid pub fn write(&self) -> Result { - let mut raw = raw::git_oid { - id: [0; raw::GIT_OID_MAX_SIZE], - }; + let mut raw = crate::util::zeroed_raw_oid(); unsafe { try_call!(raw::git_treebuilder_write(&mut raw, self.raw())); Ok(Binding::from_raw(&raw as *const _)) diff --git a/src/util.rs b/src/util.rs index 1315f0bd7a..a04858812b 100644 --- a/src/util.rs +++ b/src/util.rs @@ -268,6 +268,12 @@ fn fixup_windows_path(path: CString) -> Result { Ok(path) } +/// Creates a zeroed git_oid structure. +#[inline] +pub(crate) fn zeroed_raw_oid() -> raw::git_oid { + unsafe { std::mem::zeroed() } +} + #[cfg(test)] mod tests { use super::*;