From b79dbeb18779769fd00880050d69c050db43e028 Mon Sep 17 00:00:00 2001 From: Weihang Lo Date: Thu, 7 May 2026 15:52:11 -0400 Subject: [PATCH 01/12] feat!: `ObjectFormat` accessors and query methods This is a preparation of SHA256 support. * Added `Oid::object_format()` * Added `Oid::raw_bytes()` * Changed `Oid::from_bytes()` to use `GIT_OID_SHA1_SIZE` * Added `Repository::object_format()` * Added `Remote::object_format()` * Added `RepositoryInitOptions::object_format()` --- src/oid.rs | 45 +++++++++++++++++++++++++++++++++++---------- src/remote.rs | 17 ++++++++++++++++- src/repo.rs | 13 ++++++++++++- 3 files changed, 63 insertions(+), 12 deletions(-) diff --git a/src/oid.rs b/src/oid.rs index d1156e50d1..124c064f2e 100644 --- a/src/oid.rs +++ b/src/oid.rs @@ -65,14 +65,15 @@ impl Oid { pub fn from_bytes(bytes: &[u8]) -> Result { crate::init(); let mut raw = crate::util::zeroed_raw_oid(); - if bytes.len() != raw::GIT_OID_MAX_SIZE { - Err(Error::from_str("raw byte array must be 20 bytes")) - } else { - unsafe { - try_call!(raw::git_oid_fromraw(&mut raw, bytes.as_ptr())); - } - Ok(Oid { raw }) + + 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 }) } /// Creates an all zero Oid structure. @@ -118,7 +119,8 @@ impl Oid { Ok(Oid { raw: out }) } - /// View this OID as a byte-slice 20 bytes in length. + /// View this OID as a byte-slice in its logical length: + /// 20 bytes for SHA1, 32 bytes for SHA256. pub fn as_bytes(&self) -> &[u8] { &self.raw.id } @@ -127,6 +129,14 @@ impl Oid { pub fn is_zero(&self) -> bool { unsafe { raw::git_oid_is_zero(&self.raw) == 1 } } + + /// Returns the [`ObjectFormat`] of this OID. + /// + /// Without the `unstable-sha256` feature, this always returns + /// [`ObjectFormat::Sha1`]. + pub fn object_format(&self) -> ObjectFormat { + ObjectFormat::Sha1 + } } impl Binding for Oid { @@ -167,6 +177,9 @@ impl str::FromStr for Oid { /// Parse a hex-formatted object id into an Oid structure. /// + /// This always parses as SHA1. + /// Use [`Oid::from_str_ext`] for format-aware parsing. + /// /// # Errors /// /// Returns an error if the string is empty, is longer than 40 hex @@ -212,6 +225,8 @@ mod tests { use std::fs::File; use std::io::prelude::*; + use libgit2_sys as raw; + use super::Error; use super::Oid; use crate::ObjectType; @@ -225,6 +240,12 @@ mod tests { assert!(Oid::from_bytes(b"00000000000000000000").is_ok()); } + #[test] + fn object_format_always_sha1() { + let oid = Oid::from_bytes(&[0u8; 20]).unwrap(); + assert_eq!(oid.object_format(), crate::ObjectFormat::Sha1); + } + #[test] fn comparisons() -> Result<(), Error> { assert_eq!(Oid::from_str("decbf2b")?, Oid::from_str("decbf2b")?); @@ -259,7 +280,9 @@ mod tests { #[test] 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_SHA1_SIZE); } #[test] @@ -268,6 +291,8 @@ mod tests { 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_SHA1_SIZE); } } diff --git a/src/remote.rs b/src/remote.rs index b09bca4305..f83d23865c 100644 --- a/src/remote.rs +++ b/src/remote.rs @@ -11,7 +11,9 @@ use std::{ffi::CString, os::raw::c_char}; use crate::string_array::StringArray; use crate::util::Binding; -use crate::{call, raw, Buf, Direction, Error, FetchPrune, Oid, ProxyOptions, Refspec}; +use crate::{ + call, raw, Buf, Direction, Error, FetchPrune, ObjectFormat, Oid, ProxyOptions, Refspec, +}; use crate::{AutotagOption, Progress, RemoteCallbacks, RemoteUpdateFlags, Repository}; /// A structure representing a [remote][1] of a git repository. @@ -181,6 +183,19 @@ impl<'repo> Remote<'repo> { } } + /// Get the remote's object format (hash algorithm). + /// + /// The remote (or more exactly its transport) must have connected to the + /// remote repository. The format is available as soon as the connection to + /// the remote is initiated and it remains available after disconnecting. + pub fn object_format(&self) -> Result { + let mut oid_type = raw::GIT_OID_SHA1; + unsafe { + try_call!(raw::git_remote_oid_type(&mut oid_type, self.raw)); + Ok(Binding::from_raw(oid_type)) + } + } + /// Open a connection to a remote. pub fn connect(&mut self, dir: Direction) -> Result<(), Error> { // TODO: can callbacks be exposed safely? diff --git a/src/repo.rs b/src/repo.rs index 80f450a91f..6c5db462f8 100644 --- a/src/repo.rs +++ b/src/repo.rs @@ -124,6 +124,7 @@ pub struct RepositoryInitOptions { template_path: Option, initial_head: Option, origin_url: Option, + oid_type: Option, } impl Repository { @@ -459,7 +460,7 @@ impl Repository { } } - /// Returns the object ID format (hash algorithm) used by this repository. + /// Returns the object format (hash algorithm) of 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) } @@ -3492,6 +3493,7 @@ impl RepositoryInitOptions { template_path: None, initial_head: None, origin_url: None, + oid_type: None, } } @@ -3611,6 +3613,15 @@ impl RepositoryInitOptions { self } + /// Set the object format (hash algorithm) for the repository. + /// + /// Note: Without the `unstable-sha256` feature, this stores the format + /// but does not pass it to libgit2 (which only supports SHA1). + 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`. /// From 868b41c753f197d093dbdaa6abf56b1d39663a4b Mon Sep 17 00:00:00 2001 From: Weihang Lo Date: Thu, 7 May 2026 15:57:41 -0400 Subject: [PATCH 02/12] feat: add `*_ext` API variants Add `*_ext` method variants that accept an `ObjectFormat` param. The short-hand methods now delegate to `_ext` variants with Sha1. * Added `Oid::from_str_ext` * Added `Oid::hash_object_ext` * Added `Oid::hash_file_ext` * Added `Diff::from_buffer_ext` * Added `Index::new_ext` * Added `Index::open_ext` * Added `Indexer::new_ext` * Added `Odb::new_ext` --- src/diff.rs | 21 ++++++++++++---- src/index.rs | 23 ++++++++++++++++++ src/indexer.rs | 18 ++++++++++++++ src/odb.rs | 13 ++++++++++ src/oid.rs | 66 +++++++++++++++++++++++++++++++++++++++++--------- 5 files changed, 125 insertions(+), 16 deletions(-) diff --git a/src/diff.rs b/src/diff.rs index f65e7b26ba..e484e31b2c 100644 --- a/src/diff.rs +++ b/src/diff.rs @@ -9,6 +9,7 @@ use std::ptr; use std::slice; use crate::util::{self, Binding}; +use crate::ObjectFormat; use crate::{panic, raw, Buf, Delta, DiffFormat, Error, FileMode, Oid, Repository}; use crate::{DiffFlags, DiffStatsFormat, IntoCString}; @@ -310,16 +311,26 @@ 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. + /// + /// This parses the diff assuming SHA1 object IDs. Use + /// [`Diff::from_buffer_ext`] to specify a different format. pub fn from_buffer(buffer: &[u8]) -> Result, Error> { + Self::from_buffer_ext(buffer, ObjectFormat::Sha1) + } + + /// Reads the contents of a git patch file into a `git_diff` object, + /// with a specific object format. + /// + /// See [`Diff::from_buffer`] for more details. + pub fn from_buffer_ext(buffer: &[u8], format: 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 { + let _ = format; // 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() - )); + try_call!(raw::git_diff_from_buffer(&mut diff, data, len)); Ok(Diff::from_raw(diff)) } } diff --git a/src/index.rs b/src/index.rs index 4f72715584..0e2f3d77db 100644 --- a/src/index.rs +++ b/src/index.rs @@ -9,6 +9,7 @@ use libc::{c_char, c_int, c_uint, c_void, size_t}; use crate::util::{self, path_to_repo_path, Binding}; use crate::IntoCString; +use crate::ObjectFormat; use crate::{panic, raw, Error, IndexAddOption, IndexTime, Oid, Repository, Tree}; /// A structure to represent a git [index][1] @@ -90,10 +91,21 @@ impl Index { /// /// This index object cannot be read/written to the filesystem, but may be /// used to perform in-memory index operations. + /// + /// This always creates a SHA1 index. + /// Use [`Index::new_ext`] to create an index with a specific object format. pub fn new() -> Result { + Self::new_ext(ObjectFormat::Sha1) + } + + /// Creates a new in-memory index with a specific object format. + /// + /// See [`Index::new`] for more details. + pub fn new_ext(format: ObjectFormat) -> Result { crate::init(); let mut raw = ptr::null_mut(); unsafe { + let _ = format; try_call!(raw::git_index_new(&mut raw)); Ok(Binding::from_raw(raw)) } @@ -107,12 +119,23 @@ impl Index { /// /// If you need an index attached to a repository, use the `index()` method /// on `Repository`. + /// + /// This opens the index assuming SHA1 object format. Use + /// [`Index::open_ext`] to specify a different format. pub fn open(index_path: &Path) -> Result { + Self::open_ext(index_path, ObjectFormat::Sha1) + } + + /// Opens a Git index with a specific object format. + /// + /// See [`Index::open`] for more details. + pub fn open_ext(index_path: &Path, format: 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 { + let _ = format; try_call!(raw::git_index_open(&mut raw, index_path)); Ok(Binding::from_raw(raw)) } diff --git a/src/indexer.rs b/src/indexer.rs index 3a3ff62a5a..4ff3ddce30 100644 --- a/src/indexer.rs +++ b/src/indexer.rs @@ -6,6 +6,7 @@ use libc::c_void; use crate::odb::{write_pack_progress_cb, OdbPackwriterCb}; use crate::util::Binding; +use crate::ObjectFormat; use crate::{raw, Error, IntoCString, Odb}; /// Struct representing the progress by an in-flight transfer. @@ -123,7 +124,23 @@ 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. + /// + /// This creates an indexer assuming SHA1 object format. Use + /// [`Indexer::new_ext`] to specify a different format. pub fn new(odb: Option<&Odb<'a>>, path: &Path, mode: u32, verify: bool) -> Result { + Self::new_ext(odb, path, mode, verify, ObjectFormat::Sha1) + } + + /// Creates a new indexer with a specific object format. + /// + /// See [`Indexer::new`] for more details. + pub fn new_ext( + odb: Option<&Odb<'a>>, + path: &Path, + mode: u32, + verify: bool, + format: ObjectFormat, + ) -> Result { crate::init(); let path = path.into_c_string()?; @@ -144,6 +161,7 @@ impl<'a> Indexer<'a> { opts.progress_cb_payload = progress_payload_ptr as *mut c_void; opts.verify = verify.into(); + let _ = format; try_call!(raw::git_indexer_new(&mut out, path, mode, odb, &mut opts)); } diff --git a/src/odb.rs b/src/odb.rs index e48d3600fc..31fa7e3553 100644 --- a/src/odb.rs +++ b/src/odb.rs @@ -9,6 +9,7 @@ use libc::{c_char, c_int, c_uint, c_void, size_t}; use crate::panic; use crate::util::Binding; +use crate::ObjectFormat; use crate::{ raw, Error, IndexerProgress, Mempack, Object, ObjectType, OdbLookupFlags, Oid, Progress, }; @@ -45,10 +46,22 @@ impl<'repo> Drop for Odb<'repo> { impl<'repo> Odb<'repo> { /// Creates an object database without any backends. + /// + /// This always creates a SHA1 object database. + /// Use [`Odb::new_ext`] to create one with a specific object format. pub fn new<'a>() -> Result, Error> { + Self::new_ext(ObjectFormat::Sha1) + } + + /// Creates an object database without any backends, + /// with a specific object format. + /// + /// See [`Odb::new`] for more details. + pub fn new_ext<'a>(format: ObjectFormat) -> Result, Error> { crate::init(); unsafe { let mut out = ptr::null_mut(); + let _ = format; try_call!(raw::git_odb_new(&mut out)); Ok(Odb::from_raw(out)) } diff --git a/src/oid.rs b/src/oid.rs index 124c064f2e..698755220c 100644 --- a/src/oid.rs +++ b/src/oid.rs @@ -42,19 +42,35 @@ pub struct Oid { impl Oid { /// Parse a hex-formatted object id into an Oid structure. /// + /// This always parses as SHA1 (up to 40 hex characters). Use + /// [`Oid::from_str_ext`] to parse with a specific format. + /// /// # 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 { + Self::from_str_ext(s, ObjectFormat::Sha1) + } + + /// Parses a hex-formatted object id with a specific object format. + /// + /// # Errors + /// + /// Returns an error if the string is + /// + /// * is empty + /// * is longer than 40 hex with SHA1 object format + /// * is longer than 64 hex with SHA256 object format + /// * contains any non-hex characters + pub fn from_str_ext(s: &str, format: ObjectFormat) -> Result { crate::init(); 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 - )); + let _ = format; + try_call!(raw::git_oid_fromstrn(&mut raw, data, len)); } Ok(Oid { raw }) } @@ -86,17 +102,29 @@ impl 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. + /// + /// This always hashes using SHA1. Use [`Oid::hash_object_ext`] + /// to hash with a specific format. pub fn hash_object(kind: ObjectType, bytes: &[u8]) -> Result { + Self::hash_object_ext(kind, bytes, ObjectFormat::Sha1) + } + + /// Hashes the provided data as an object of the provided type, + /// with a specific object format. + /// + /// See [`Oid::hash_object`] for more details. + pub fn hash_object_ext( + kind: ObjectType, + bytes: &[u8], + format: ObjectFormat, + ) -> Result { crate::init(); let mut out = crate::util::zeroed_raw_oid(); + let data = bytes.as_ptr() as *const libc::c_void; unsafe { - try_call!(raw::git_odb_hash( - &mut out, - bytes.as_ptr() as *const libc::c_void, - bytes.len(), - kind.raw() - )); + let _ = format; + try_call!(raw::git_odb_hash(&mut out, data, bytes.len(), kind.raw())); } Ok(Oid { raw: out }) @@ -105,7 +133,22 @@ 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. + /// + /// This always hashes using SHA1. Use [`Oid::hash_file_ext`] + /// to hash with a specific format. pub fn hash_file>(kind: ObjectType, path: P) -> Result { + Self::hash_file_ext(kind, path, ObjectFormat::Sha1) + } + + /// Hashes the content of a file as an object of the provided type, + /// with a specific object format. + /// + /// See [`Oid::hash_file`] for more details. + pub fn hash_file_ext>( + kind: ObjectType, + path: P, + format: ObjectFormat, + ) -> Result { crate::init(); // Normal file path OK (does not need Windows conversion). @@ -113,6 +156,7 @@ impl Oid { let mut out = crate::util::zeroed_raw_oid(); unsafe { + let _ = format; try_call!(raw::git_odb_hashfile(&mut out, rpath, kind.raw())); } From 864bfec4d19d55d2f46dcbbb1e6f89481a07f0b7 Mon Sep 17 00:00:00 2001 From: Weihang Lo Date: Thu, 7 May 2026 17:58:51 -0400 Subject: [PATCH 03/12] refactor: format-aware OID parsing via `*_ext` fn This makes these call sites automatically handle the correct object format once SHA256 support is wired up. --- examples/diff.rs | 2 +- src/commit.rs | 5 +++++ src/index.rs | 8 +++++--- src/odb.rs | 2 +- src/remote.rs | 3 ++- src/repo.rs | 9 ++++++--- 6 files changed, 20 insertions(+), 9 deletions(-) diff --git a/examples/diff.rs b/examples/diff.rs index 7440149ba0..87f9da5c11 100644 --- a/examples/diff.rs +++ b/examples/diff.rs @@ -319,7 +319,7 @@ 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) => Oid::from_str_ext(s, repo.object_format())?, None => return Ok(None), }; repo.find_blob(arg).map(|b| Some(b)) diff --git a/src/commit.rs b/src/commit.rs index c1fe337dbe..dda78d13e0 100644 --- a/src/commit.rs +++ b/src/commit.rs @@ -442,6 +442,11 @@ mod tests { 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(); + crate::Oid::from_str_ext(str, repo.object_format()).unwrap() + }; + assert_eq!(tree_oid, commit.tree_id()); assert_eq!(commit.author().name(), Ok("name")); assert_eq!(commit.author().email(), Ok("email")); assert_eq!(commit.committer().name(), Ok("name")); diff --git a/src/index.rs b/src/index.rs index 0e2f3d77db..779d80dcdc 100644 --- a/src/index.rs +++ b/src/index.rs @@ -869,7 +869,9 @@ mod tests { use std::path::Path; use tempfile::TempDir; - use crate::{ErrorCode, Index, IndexEntry, IndexTime, Oid, Repository, ResetType}; + use crate::{ + ErrorCode, Index, IndexEntry, IndexTime, ObjectFormat, Oid, Repository, ResetType, + }; #[test] fn smoke() { @@ -972,7 +974,7 @@ mod tests { #[test] fn add_then_read() { - let mut index = Index::new().unwrap(); + let mut index = Index::new_ext(ObjectFormat::Sha1).unwrap(); let mut e = entry(); e.path = b"foobar".to_vec(); index.add(&e).unwrap(); @@ -982,7 +984,7 @@ mod tests { #[test] fn add_then_find() { - let mut index = Index::new().unwrap(); + let mut index = Index::new_ext(ObjectFormat::Sha1).unwrap(); let mut e = entry(); e.path = b"foo/bar".to_vec(); index.add(&e).unwrap(); diff --git a/src/odb.rs b/src/odb.rs index 31fa7e3553..1789424857 100644 --- a/src/odb.rs +++ b/src/odb.rs @@ -648,7 +648,7 @@ 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 = Oid::from_str_ext(id_prefix_str, repo.object_format()).unwrap(); let found_oid = db.exists_prefix(id_prefix, 10).unwrap(); assert_eq!(found_oid, id); } diff --git a/src/remote.rs b/src/remote.rs index f83d23865c..0037b53975 100644 --- a/src/remote.rs +++ b/src/remote.rs @@ -825,7 +825,7 @@ impl RemoteRedirect { #[cfg(test)] mod tests { use crate::{AutotagOption, PushOptions, RemoteUpdateFlags}; - use crate::{Direction, FetchOptions, Remote, RemoteCallbacks, Repository}; + use crate::{Direction, FetchOptions, ObjectFormat, Remote, RemoteCallbacks, Repository}; use std::cell::Cell; use tempfile::TempDir; @@ -895,6 +895,7 @@ mod tests { origin.connect(Direction::Fetch).unwrap(); assert!(origin.connected()); + assert_eq!(origin.object_format().unwrap(), ObjectFormat::Sha1); origin.download(&[] as &[&str], None).unwrap(); origin.disconnect().unwrap(); diff --git a/src/repo.rs b/src/repo.rs index 6c5db462f8..4603ad1f4d 100644 --- a/src/repo.rs +++ b/src/repo.rs @@ -1473,11 +1473,12 @@ impl Repository { /// Lookup a reference to one of the commits in a repository by short hash. pub fn find_commit_by_prefix(&self, prefix_hash: &str) -> Result, Error> { let mut raw = ptr::null_mut(); + let oid = Oid::from_str_ext(prefix_hash, self.object_format())?; unsafe { try_call!(raw::git_commit_lookup_prefix( &mut raw, self.raw(), - Oid::from_str(prefix_hash)?.raw(), + oid.raw(), prefix_hash.len() )); Ok(Binding::from_raw(raw)) @@ -1518,11 +1519,12 @@ impl Repository { kind: Option, ) -> Result, Error> { let mut raw = ptr::null_mut(); + let oid = Oid::from_str_ext(prefix_hash, self.object_format())?; unsafe { try_call!(raw::git_object_lookup_prefix( &mut raw, self.raw(), - Oid::from_str(prefix_hash)?.raw(), + oid.raw(), prefix_hash.len(), kind )); @@ -2093,11 +2095,12 @@ impl Repository { /// Lookup a tag object by prefix hash from the repository. pub fn find_tag_by_prefix(&self, prefix_hash: &str) -> Result, Error> { let mut raw = ptr::null_mut(); + let oid = Oid::from_str_ext(prefix_hash, self.object_format())?; unsafe { try_call!(raw::git_tag_lookup_prefix( &mut raw, self.raw, - Oid::from_str(prefix_hash)?.raw(), + oid.raw(), prefix_hash.len() )); Ok(Binding::from_raw(raw)) From 58b50a8a9362db9be434c29b25c2e07cf76dffc9 Mon Sep 17 00:00:00 2001 From: Weihang Lo Date: Wed, 6 May 2026 23:21:25 -0400 Subject: [PATCH 04/12] feat: SHA256 support behind unstable-sha256 feature Declare the `unstable-sha256` Cargo feature, reflecting upstream libgit2's `GIT_EXPERIMENTAL_SHA256`. This is an ABI-breaking experimental feature. Implement the SHA256 support gated behind this feature: Things gated: * `ObjectFormat::Sha256` and non_exhaustive when `unstable-sha256` is off * all FFI call sites for the overloaded libgit2 function signatures * git_oid_fromstrn * git_oid_fromraw, * git_odb_hash * git_odb_hashfile * git_diff_from_buffer, * git_index_new * git_index_open * git_indexer_new * git_odb_new * `Repository::object_format` is format-aware * `Remote::object_format` is format-aware * `Oid::{as_bytes,object_format,from_bytes}` are format-aware --- Cargo.toml | 16 +++ src/commit.rs | 41 ++++++ src/diff.rs | 51 +++++++- src/index.rs | 56 +++++++- src/indexer.rs | 52 +++++++- src/odb.rs | 17 ++- src/oid.rs | 320 +++++++++++++++++++++++++++++++++++++++++++-- src/packbuilder.rs | 154 ++++++++++++++++++++-- src/repo.rs | 141 +++++++++++++++++++- src/test.rs | 14 +- src/transaction.rs | 38 +++++- 11 files changed, 859 insertions(+), 41 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 0e1747cdea..7df32eacfa 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,6 +34,22 @@ 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. +# +# NOTE: libgit2's builtin SHA256 implementation is not thread-safe. +# Enable the `https` feature to route hashing through thread-safe OpenSSL impl. +# See https://github.com/rust-lang/git2-rs/issues/1255. +unstable-sha256 = ["libgit2-sys/unstable-sha256"] default = [] ssh = ["libgit2-sys/ssh", "cred"] https = ["libgit2-sys/https", "openssl-sys", "openssl-probe", "cred"] diff --git a/src/commit.rs b/src/commit.rs index dda78d13e0..c3c4104051 100644 --- a/src/commit.rs +++ b/src/commit.rs @@ -473,4 +473,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(), Ok("initial\n\nbody")); + assert_eq!(commit.body(), Ok(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_ext(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 e484e31b2c..3d2cd04543 100644 --- a/src/diff.rs +++ b/src/diff.rs @@ -327,10 +327,20 @@ impl Diff<'static> { let mut diff: *mut raw::git_diff = std::ptr::null_mut(); let data = buffer.as_ptr() as *const c_char; let len = buffer.len(); + // NOTE: Doesn't depend on repo, so lifetime can be 'static unsafe { - let _ = format; - // NOTE: Doesn't depend on repo, so lifetime can be 'static - try_call!(raw::git_diff_from_buffer(&mut diff, data, len)); + #[cfg(not(feature = "unstable-sha256"))] + { + let _ = format; + 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)) } } @@ -1563,6 +1573,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; @@ -1869,4 +1881,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_ext(patch, crate::ObjectFormat::Sha256).unwrap(); + assert_eq!(diff.deltas().len(), 1); + } } diff --git a/src/index.rs b/src/index.rs index 779d80dcdc..9532ff8461 100644 --- a/src/index.rs +++ b/src/index.rs @@ -105,8 +105,18 @@ impl Index { crate::init(); let mut raw = ptr::null_mut(); unsafe { - let _ = format; - try_call!(raw::git_index_new(&mut raw)); + #[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)) } } @@ -135,8 +145,18 @@ impl Index { // Normal file path OK (does not need Windows conversion). let index_path = index_path.into_c_string()?; unsafe { - let _ = format; - try_call!(raw::git_index_open(&mut raw, index_path)); + #[cfg(not(feature = "unstable-sha256"))] + { + let _ = format; + 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)) } } @@ -1029,10 +1049,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_ext(&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 smoke_in_memory_index_sha256() { + let _index = Index::new_ext(ObjectFormat::Sha256).unwrap(); + } } diff --git a/src/indexer.rs b/src/indexer.rs index 4ff3ddce30..f6efe6ac65 100644 --- a/src/indexer.rs +++ b/src/indexer.rs @@ -161,8 +161,18 @@ impl<'a> Indexer<'a> { opts.progress_cb_payload = progress_payload_ptr as *mut c_void; opts.verify = verify.into(); - let _ = format; - try_call!(raw::git_indexer_new(&mut out, path, mode, odb, &mut opts)); + #[cfg(not(feature = "unstable-sha256"))] + { + let _ = format; + try_call!(raw::git_indexer_new(&mut out, path, mode, odb, &mut opts)); + } + #[cfg(feature = "unstable-sha256")] + { + opts.mode = mode; + opts.oid_type = format.raw(); + opts.odb = odb; + try_call!(raw::git_indexer_new(&mut out, path, &mut opts)); + } } Ok(Self { @@ -270,4 +280,42 @@ mod tests { 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_ext( + Some(&odb), + repo_target.path().join("objects").join("pack").as_path(), + 0o644, + true, + crate::ObjectFormat::Sha256, + ) + .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); + } } diff --git a/src/odb.rs b/src/odb.rs index 1789424857..c6900403fd 100644 --- a/src/odb.rs +++ b/src/odb.rs @@ -61,8 +61,18 @@ impl<'repo> Odb<'repo> { crate::init(); unsafe { let mut out = ptr::null_mut(); - let _ = format; - try_call!(raw::git_odb_new(&mut out)); + #[cfg(not(feature = "unstable-sha256"))] + { + let _ = format; + try_call!(raw::git_odb_new(&mut out)); + } + #[cfg(feature = "unstable-sha256")] + { + 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)) } } @@ -252,7 +262,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::new_ext(git2::ObjectFormat::Sha1).unwrap(); /// odb.add_new_mempack_backend(1000).unwrap() /// }; /// ``` diff --git a/src/oid.rs b/src/oid.rs index 698755220c..9f81c35a70 100644 --- a/src/oid.rs +++ b/src/oid.rs @@ -10,9 +10,13 @@ use crate::util::{c_cmp_to_ordering, Binding}; /// Object ID format (hash algorithm). #[derive(Debug, Copy, Clone, PartialEq, Eq)] +#[cfg_attr(not(feature = "unstable-sha256"), non_exhaustive)] 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 { @@ -21,6 +25,8 @@ impl Binding for ObjectFormat { 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"), } } @@ -28,6 +34,8 @@ impl Binding for ObjectFormat { fn raw(&self) -> Self::Raw { match self { ObjectFormat::Sha1 => raw::GIT_OID_SHA1, + #[cfg(feature = "unstable-sha256")] + ObjectFormat::Sha256 => raw::GIT_OID_SHA256, } } } @@ -69,8 +77,13 @@ impl Oid { let data = s.as_bytes().as_ptr() as *const libc::c_char; let len = s.len() as libc::size_t; unsafe { - let _ = format; - try_call!(raw::git_oid_fromstrn(&mut raw, data, len)); + #[cfg(not(feature = "unstable-sha256"))] + { + let _ = format; + 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 }) } @@ -82,11 +95,34 @@ impl Oid { crate::init(); let mut raw = crate::util::zeroed_raw_oid(); - if bytes.len() != raw::GIT_OID_SHA1_SIZE { - return Err(Error::from_str("raw byte array must be 20 bytes")); + #[cfg(not(feature = "unstable-sha256"))] + { + if bytes.len() != raw::GIT_OID_SHA1_SIZE { + return Err(Error::from_str(&format!( + "raw byte array must be 20 bytes, but got {}", + bytes.len() + ))); + } + unsafe { + try_call!(raw::git_oid_fromraw(&mut raw, bytes.as_ptr())); + } } - unsafe { - try_call!(raw::git_oid_fromraw(&mut raw, bytes.as_ptr())); + + #[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(&format!( + "raw byte array must be 20 bytes (SHA1) or 32 bytes (SHA256), but got {}", + bytes.len() + ))); + } + }; + unsafe { + try_call!(raw::git_oid_fromraw(&mut raw, bytes.as_ptr(), oid_type)); + } } Ok(Oid { raw }) @@ -123,8 +159,19 @@ impl Oid { let mut out = crate::util::zeroed_raw_oid(); let data = bytes.as_ptr() as *const libc::c_void; unsafe { - let _ = format; - try_call!(raw::git_odb_hash(&mut out, data, bytes.len(), kind.raw())); + #[cfg(not(feature = "unstable-sha256"))] + { + let _ = format; + 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, + data, + bytes.len(), + kind.raw(), + format.raw() + )); } Ok(Oid { raw: out }) @@ -156,8 +203,18 @@ impl Oid { let mut out = crate::util::zeroed_raw_oid(); unsafe { - let _ = format; - try_call!(raw::git_odb_hashfile(&mut out, rpath, kind.raw())); + #[cfg(not(feature = "unstable-sha256"))] + { + let _ = format; + 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 }) @@ -166,7 +223,19 @@ impl Oid { /// View this OID as a byte-slice in its logical length: /// 20 bytes for SHA1, 32 bytes for SHA256. pub fn as_bytes(&self) -> &[u8] { - &self.raw.id + #[cfg(not(feature = "unstable-sha256"))] + { + &self.raw.id + } + #[cfg(feature = "unstable-sha256")] + { + let size = match self.raw.kind as raw::git_oid_t { + raw::GIT_OID_SHA1 => raw::GIT_OID_SHA1_SIZE, + raw::GIT_OID_SHA256 => raw::GIT_OID_SHA256_SIZE, + _ => panic!("Unknown git oid type"), + }; + &self.raw.id[..size] + } } /// Test if this OID is all zeros. @@ -179,7 +248,14 @@ impl Oid { /// Without the `unstable-sha256` feature, this always returns /// [`ObjectFormat::Sha1`]. pub fn object_format(&self) -> ObjectFormat { - ObjectFormat::Sha1 + #[cfg(not(feature = "unstable-sha256"))] + { + ObjectFormat::Sha1 + } + #[cfg(feature = "unstable-sha256")] + { + unsafe { Binding::from_raw(self.raw.kind as raw::git_oid_t) } + } } } @@ -254,6 +330,8 @@ impl Ord for Oid { impl Hash for Oid { fn hash(&self, into: &mut H) { + #[cfg(feature = "unstable-sha256")] + self.raw.kind.hash(into); self.raw.id.hash(into) } } @@ -284,6 +362,49 @@ mod tests { assert!(Oid::from_bytes(b"00000000000000000000").is_ok()); } + #[test] + #[cfg(feature = "unstable-sha256")] + fn conversions_object_format() { + use crate::ObjectFormat; + + assert!(Oid::from_str_ext("foo", ObjectFormat::Sha1).is_err()); + assert!(Oid::from_str_ext( + "decbf2be529ab6557d5429922251e5ee36519817", + ObjectFormat::Sha1 + ) + .is_ok()); + + assert!(Oid::from_str_ext("foo", ObjectFormat::Sha256).is_err()); + assert!(Oid::from_str_ext( + "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(); + + // as_bytes() returns logical length per OID type + assert_eq!(sha1_from_bytes.as_bytes().len(), raw::GIT_OID_SHA1_SIZE); + assert_eq!(sha256_from_bytes.as_bytes().len(), raw::GIT_OID_SHA256_SIZE); + + // raw_bytes() always returns the full buffer + assert_eq!(sha1_from_bytes.raw_bytes().len(), raw::GIT_OID_MAX_SIZE); + assert_eq!(sha256_from_bytes.raw_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] fn object_format_always_sha1() { let oid = Oid::from_bytes(&[0u8; 20]).unwrap(); @@ -291,6 +412,33 @@ mod tests { } #[test] + #[cfg(feature = "unstable-sha256")] + fn object_format_from_oid() { + use crate::ObjectFormat; + + let sha1 = Oid::from_bytes(&[0u8; 20]).unwrap(); + assert_eq!(sha1.object_format(), ObjectFormat::Sha1); + + let sha256 = Oid::from_bytes(&[0u8; 32]).unwrap(); + assert_eq!(sha256.object_format(), ObjectFormat::Sha256); + + let sha1_from_str = Oid::from_str_ext( + "decbf2be529ab6557d5429922251e5ee36519817", + ObjectFormat::Sha1, + ) + .unwrap(); + assert_eq!(sha1_from_str.object_format(), ObjectFormat::Sha1); + + let sha256_from_str = Oid::from_str_ext( + "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", + ObjectFormat::Sha256, + ) + .unwrap(); + assert_eq!(sha256_from_str.object_format(), ObjectFormat::Sha256); + } + + #[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")?); @@ -316,6 +464,109 @@ mod tests { 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_str_ext("decbf2b", ObjectFormat::Sha1)?, + Oid::from_str_ext("decbf2b", ObjectFormat::Sha1)? + ); + assert!( + Oid::from_str_ext("decbf2b", ObjectFormat::Sha1)? + <= Oid::from_str_ext("decbf2b", ObjectFormat::Sha1)? + ); + assert!( + Oid::from_str_ext("decbf2b", ObjectFormat::Sha1)? + >= Oid::from_str_ext("decbf2b", ObjectFormat::Sha1)? + ); + { + let o = Oid::from_str_ext("decbf2b", ObjectFormat::Sha1)?; + assert_eq!(o, o); + assert!(o <= o); + assert!(o >= o); + } + assert_eq!( + Oid::from_str_ext("decbf2b", ObjectFormat::Sha1)?, + Oid::from_str_ext( + "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_ext("decbf2b", ObjectFormat::Sha1)? + ); + + // SHA256 OID comparisons with explicit format (using full 64-char hex strings) + assert_eq!( + Oid::from_str_ext( + "abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890", + ObjectFormat::Sha256 + )?, + Oid::from_str_ext( + "abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890", + ObjectFormat::Sha256 + )? + ); + assert!( + Oid::from_str_ext( + "abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890", + ObjectFormat::Sha256 + )? <= Oid::from_str_ext( + "abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890", + ObjectFormat::Sha256 + )? + ); + assert!( + Oid::from_str_ext( + "abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890", + ObjectFormat::Sha256 + )? >= Oid::from_str_ext( + "abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890", + ObjectFormat::Sha256 + )? + ); + { + let o = Oid::from_str_ext( + "abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890", + ObjectFormat::Sha256, + )?; + assert_eq!(o, o); + assert!(o <= o); + assert!(o >= o); + } + assert_eq!( + Oid::from_str_ext("abcdef12", ObjectFormat::Sha256)?, + Oid::from_str_ext( + "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_ext( + "abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890", + ObjectFormat::Sha256 + )? + ); + + Ok(()) + } + #[test] fn zero_is_zero() { assert!(Oid::zero().is_zero()); @@ -329,6 +580,27 @@ mod tests { assert_eq!(oid.as_bytes().len(), raw::GIT_OID_SHA1_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_ext(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_SHA1_SIZE); + + let sha256_oid = Oid::hash_object_ext(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_SHA256_SIZE); + + // Different formats produce different OIDs + assert_ne!(sha1_oid, sha256_oid); + + Ok(()) + } + #[test] fn hash_file() { let td = TempDir::new().unwrap(); @@ -339,4 +611,28 @@ mod tests { assert_eq!(oid.to_string().len(), raw::GIT_OID_SHA1_HEXSIZE); assert_eq!(oid.as_bytes().len(), raw::GIT_OID_SHA1_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_file_ext(ObjectType::Blob, &path, ObjectFormat::Sha1)?; + assert_eq!(sha1_oid.to_string().len(), raw::GIT_OID_SHA1_HEXSIZE); + assert_eq!(sha1_oid.as_bytes().len(), raw::GIT_OID_SHA1_SIZE); + + let sha256_oid = Oid::hash_file_ext(ObjectType::Blob, &path, ObjectFormat::Sha256)?; + assert_eq!(sha256_oid.to_string().len(), raw::GIT_OID_SHA256_HEXSIZE); + assert_eq!(sha256_oid.as_bytes().len(), raw::GIT_OID_SHA256_SIZE); + + // Different formats produce different OIDs + assert_ne!(sha1_oid, sha256_oid); + + Ok(()) + } } diff --git a/src/packbuilder.rs b/src/packbuilder.rs index 06cfd6c4df..6595f0e396 100644 --- a/src/packbuilder.rs +++ b/src/packbuilder.rs @@ -295,10 +295,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() @@ -316,8 +313,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::>() } @@ -342,16 +356,45 @@ 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().expect("Should return Ok result").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() == Some(EMPTY_PACKFILE_OID)); + assert_eq!(builder.name().unwrap(), Some(EMPTY_PACKFILE_OID)); + } + + #[test] + #[cfg(feature = "unstable-sha256")] + 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(), Some(EMPTY_PACKFILE_OID_SHA256)); } #[test] @@ -366,6 +409,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(); @@ -379,6 +435,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(); @@ -393,6 +463,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(); @@ -407,6 +492,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(); @@ -418,6 +518,18 @@ mod tests { t!(repo.find_commit(commit)); } + #[test] + #[cfg(feature = "unstable-sha256")] + 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(); @@ -430,6 +542,19 @@ mod tests { t!(repo.find_tree(tree)); } + #[test] + #[cfg(feature = "unstable-sha256")] + 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(); @@ -442,6 +567,19 @@ mod tests { t!(repo.find_commit(commit)); } + #[test] + #[cfg(feature = "unstable-sha256")] + 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 4603ad1f4d..ac0ac9d595 100644 --- a/src/repo.rs +++ b/src/repo.rs @@ -3618,8 +3618,9 @@ impl RepositoryInitOptions { /// Set the object format (hash algorithm) for the repository. /// - /// Note: Without the `unstable-sha256` feature, this stores the format - /// but does not pass it to libgit2 (which only supports SHA1). + /// The default is [`ObjectFormat::Sha1`]. + /// Setting this to [`ObjectFormat::Sha256`] (requires `unstable-sha256`) + /// will create a repository that uses SHA256 object IDs. pub fn object_format(&mut self, format: ObjectFormat) -> &mut RepositoryInitOptions { self.oid_type = Some(format.raw()); self @@ -3646,6 +3647,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 } } @@ -3654,6 +3659,8 @@ impl RepositoryInitOptions { mod tests { use crate::build::CheckoutBuilder; use crate::ObjectFormat; + #[cfg(feature = "unstable-sha256")] + use crate::RepositoryInitOptions; use crate::{CherrypickOptions, MergeFileOptions}; use crate::{ Config, ObjectType, Oid, Repository, ResetType, Signature, SubmoduleIgnore, SubmoduleUpdate, @@ -3676,10 +3683,32 @@ mod tests { assert_eq!(repo.object_format(), ObjectFormat::Sha1); let oid = repo.blob(b"test").unwrap(); - assert_eq!(oid.as_bytes().len(), raw::GIT_OID_MAX_SIZE); + assert_eq!(oid.as_bytes().len(), raw::GIT_OID_SHA1_SIZE); 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").unwrap(); + assert_eq!(oid.as_bytes().len(), raw::GIT_OID_SHA256_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] fn smoke_init_bare() { let td = TempDir::new().unwrap(); @@ -3691,6 +3720,22 @@ mod tests { assert_eq!(repo.object_format(), ObjectFormat::Sha1); } + #[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().expect("Okay even if none").is_none()); + assert_eq!(repo.object_format(), ObjectFormat::Sha256); + } + #[test] fn smoke_open() { let td = TempDir::new().unwrap(); @@ -3707,10 +3752,39 @@ mod tests { assert_eq!(repo.state(), crate::RepositoryState::Clean); let oid = repo.blob(b"test").unwrap(); - assert_eq!(oid.as_bytes().len(), raw::GIT_OID_MAX_SIZE); + assert_eq!(oid.as_bytes().len(), raw::GIT_OID_SHA1_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").unwrap(); + assert_eq!(oid.as_bytes().len(), raw::GIT_OID_SHA256_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] fn smoke_open_bare() { let td = TempDir::new().unwrap(); @@ -3725,12 +3799,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(); @@ -3748,6 +3853,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(); @@ -3926,7 +4053,11 @@ mod tests { fn smoke_set_head_detached() { let (_td, repo) = crate::test::repo_init(); - let void_oid = Oid::from_bytes(b"00000000000000000000").unwrap(); + let void_oid = match repo.object_format() { + ObjectFormat::Sha1 => Oid::from_bytes(&[0; raw::GIT_OID_SHA1_SIZE]).unwrap(), + #[cfg(feature = "unstable-sha256")] + ObjectFormat::Sha256 => Oid::from_bytes(&[0; raw::GIT_OID_SHA256_SIZE]).unwrap(), + }; assert!(repo.set_head_detached(void_oid).is_err()); let main_oid = repo.revparse_single("main").unwrap().id(); diff --git a/src/test.rs b/src/test.rs index 57a590f519..a1a303d6cc 100644 --- a/src/test.rs +++ b/src/test.rs @@ -6,7 +6,7 @@ use std::ptr; use tempfile::TempDir; use url::Url; -use crate::{Branch, Oid, Repository, RepositoryInitOptions}; +use crate::{Branch, ObjectFormat, Oid, Repository, RepositoryInitOptions}; macro_rules! t { ($e:expr) => { @@ -17,10 +17,11 @@ macro_rules! t { }; } -pub fn repo_init() -> (TempDir, Repository) { +fn repo_init_ext(format: ObjectFormat) -> (TempDir, Repository) { let td = TempDir::new().unwrap(); let mut opts = RepositoryInitOptions::new(); opts.initial_head("main"); + opts.object_format(format); let repo = Repository::init_opts(td.path(), &opts).unwrap(); { let mut config = repo.config().unwrap(); @@ -37,6 +38,15 @@ pub fn repo_init() -> (TempDir, Repository) { (td, repo) } +pub fn repo_init() -> (TempDir, Repository) { + repo_init_ext(ObjectFormat::Sha1) +} + +#[cfg(feature = "unstable-sha256")] +pub fn repo_init_sha256() -> (TempDir, Repository) { + repo_init_ext(ObjectFormat::Sha256) +} + 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 e401a16e71..2c8f9d7011 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,13 +173,44 @@ 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(), - Some("refs/heads/main") + Some("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() + .symbolic_target() + .unwrap(), + Some("refs/heads/main"), ); } From d6406a14385ff5b84d6caee78c2a0350997b0200 Mon Sep 17 00:00:00 2001 From: Weihang Lo Date: Thu, 7 May 2026 19:20:10 -0400 Subject: [PATCH 05/12] feat: deprecate `Oid::zero()` in favor of consts `Oid::zero().object_format()` panics with `unstable-sha256` because the raw `kind` field was left as zero. The zeroed struct is generally use for output param. Not really meaning to switch a `zero_ext` variant. Replace it with const values for both kinds. --- src/diff.rs | 2 +- src/index.rs | 4 ++-- src/oid.rs | 48 ++++++++++++++++++++++++++++++++++++---------- src/repo.rs | 4 ++-- src/transaction.rs | 2 +- 5 files changed, 44 insertions(+), 16 deletions(-) diff --git a/src/diff.rs b/src/diff.rs index 3d2cd04543..38b6ed8256 100644 --- a/src/diff.rs +++ b/src/diff.rs @@ -1591,7 +1591,7 @@ mod tests { assert_eq!(stats.deletions(), 0); assert_eq!(stats.files_changed(), 0); let patchid = diff.patchid(None).unwrap(); - assert_ne!(patchid, Oid::zero()); + assert_ne!(patchid, Oid::ZERO_SHA1); } #[test] diff --git a/src/index.rs b/src/index.rs index 9532ff8461..bed3dba22a 100644 --- a/src/index.rs +++ b/src/index.rs @@ -1050,9 +1050,9 @@ mod tests { gid: 0, file_size: 0, #[cfg(not(feature = "unstable-sha256"))] - id: Oid::from_bytes(&[0; 20]).unwrap(), + id: Oid::ZERO_SHA1, #[cfg(feature = "unstable-sha256")] - id: Oid::from_bytes(&[0; 32]).unwrap(), + id: Oid::ZERO_SHA256, flags: 0, flags_extended: 0, path: Vec::new(), diff --git a/src/oid.rs b/src/oid.rs index 9f81c35a70..39a731a4f8 100644 --- a/src/oid.rs +++ b/src/oid.rs @@ -48,6 +48,24 @@ pub struct Oid { } impl Oid { + /// An all-zero SHA1 OID. + pub const ZERO_SHA1: Oid = Oid { + raw: raw::git_oid { + #[cfg(feature = "unstable-sha256")] + kind: raw::GIT_OID_SHA1 as libc::c_uchar, + id: [0; raw::GIT_OID_MAX_SIZE], + }, + }; + + /// An all-zero SHA256 OID. + #[cfg(feature = "unstable-sha256")] + pub const ZERO_SHA256: Oid = Oid { + raw: raw::git_oid { + kind: raw::GIT_OID_SHA256 as libc::c_uchar, + id: [0; raw::GIT_OID_MAX_SIZE], + }, + }; + /// Parse a hex-formatted object id into an Oid structure. /// /// This always parses as SHA1 (up to 40 hex characters). Use @@ -128,11 +146,10 @@ impl Oid { Ok(Oid { raw }) } - /// Creates an all zero Oid structure. + /// Creates an all-zero SHA1 OID. + #[deprecated(since = "0.21.0", note = "use `Oid::ZERO_SHA1` instead")] pub fn zero() -> Oid { - Oid { - raw: crate::util::zeroed_raw_oid(), - } + Self::ZERO_SHA1 } /// Hashes the provided data as an object of the provided type, and returns @@ -383,8 +400,8 @@ mod tests { 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(); + let sha1_from_bytes = Oid::ZERO_SHA1; + let sha256_from_bytes = Oid::ZERO_SHA256; // as_bytes() returns logical length per OID type assert_eq!(sha1_from_bytes.as_bytes().len(), raw::GIT_OID_SHA1_SIZE); @@ -407,7 +424,7 @@ mod tests { #[test] fn object_format_always_sha1() { - let oid = Oid::from_bytes(&[0u8; 20]).unwrap(); + let oid = Oid::ZERO_SHA1; assert_eq!(oid.object_format(), crate::ObjectFormat::Sha1); } @@ -416,10 +433,10 @@ mod tests { fn object_format_from_oid() { use crate::ObjectFormat; - let sha1 = Oid::from_bytes(&[0u8; 20]).unwrap(); + let sha1 = Oid::ZERO_SHA1; assert_eq!(sha1.object_format(), ObjectFormat::Sha1); - let sha256 = Oid::from_bytes(&[0u8; 32]).unwrap(); + let sha256 = Oid::ZERO_SHA256; assert_eq!(sha256.object_format(), ObjectFormat::Sha256); let sha1_from_str = Oid::from_str_ext( @@ -569,7 +586,18 @@ mod tests { #[test] fn zero_is_zero() { - assert!(Oid::zero().is_zero()); + assert!(Oid::ZERO_SHA1.is_zero()); + assert_eq!(Oid::ZERO_SHA1.object_format(), crate::ObjectFormat::Sha1); + } + + #[test] + #[cfg(feature = "unstable-sha256")] + fn zero_sha256_is_zero() { + assert!(Oid::ZERO_SHA256.is_zero()); + assert_eq!( + Oid::ZERO_SHA256.object_format(), + crate::ObjectFormat::Sha256 + ); } #[test] diff --git a/src/repo.rs b/src/repo.rs index ac0ac9d595..7a61be161a 100644 --- a/src/repo.rs +++ b/src/repo.rs @@ -4054,9 +4054,9 @@ mod tests { let (_td, repo) = crate::test::repo_init(); let void_oid = match repo.object_format() { - ObjectFormat::Sha1 => Oid::from_bytes(&[0; raw::GIT_OID_SHA1_SIZE]).unwrap(), + ObjectFormat::Sha1 => Oid::ZERO_SHA1, #[cfg(feature = "unstable-sha256")] - ObjectFormat::Sha256 => Oid::from_bytes(&[0; raw::GIT_OID_SHA256_SIZE]).unwrap(), + ObjectFormat::Sha256 => Oid::ZERO_SHA256, }; assert!(repo.set_head_detached(void_oid).is_err()); diff --git a/src/transaction.rs b/src/transaction.rs index 2c8f9d7011..d4116b050f 100644 --- a/src/transaction.rs +++ b/src/transaction.rs @@ -310,7 +310,7 @@ mod tests { let mut tx = t!(repo.transaction()); assert!(matches!( - tx.set_target("refs/heads/main", Oid::zero(), None, "set main to zero"), + tx.set_target("refs/heads/main", Oid::ZERO_SHA1, None, "set main to zero"), Err(e) if is_not_locked_err(&e) )) } From 9fb4e6aeaeb17868c5e86e8ee67545c57d070ced Mon Sep 17 00:00:00 2001 From: Weihang Lo Date: Fri, 8 May 2026 08:21:48 -0400 Subject: [PATCH 06/12] refactor(util): drop `unsafe` from `zeroed_raw_oid` Construct `git_oid` with a struct literal instead of `mem::zeroed`. Under `unstable-sha256`, default `kind` to `GIT_OID_SHA1` so the value is a valid `git_oid_t` even before libgit2 overwrites it as an output buffer. --- src/util.rs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/util.rs b/src/util.rs index a04858812b..c9cfaa6740 100644 --- a/src/util.rs +++ b/src/util.rs @@ -268,10 +268,18 @@ fn fixup_windows_path(path: CString) -> Result { Ok(path) } -/// Creates a zeroed git_oid structure. +/// Creates a zeroed `git_oid` to be used as an output buffer +/// that libgit2 overwrites before any read. +/// +/// Defaults `kind` to `GIT_OID_SHA1` so the value is a valid +/// `git_oid_t` even transiently. #[inline] pub(crate) fn zeroed_raw_oid() -> raw::git_oid { - unsafe { std::mem::zeroed() } + raw::git_oid { + #[cfg(feature = "unstable-sha256")] + kind: raw::GIT_OID_SHA1 as libc::c_uchar, + id: [0; raw::GIT_OID_MAX_SIZE], + } } #[cfg(test)] From bdf5b7bf4aa13b9a2ed6e85d8d2d884b83fffcc7 Mon Sep 17 00:00:00 2001 From: Weihang Lo Date: Wed, 6 May 2026 19:32:44 -0400 Subject: [PATCH 07/12] fix: init example with `--object-format` option --- examples/init.rs | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/examples/init.rs b/examples/init.rs index 3ae79082d7..6a3bf2ed53 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 to use the sha256 format) + 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) { From 7acee09983d45fb6b3c0680e59838fb9619e5149 Mon Sep 17 00:00:00 2001 From: Weihang Lo Date: Wed, 6 May 2026 19:32:49 -0400 Subject: [PATCH 08/12] chore(ci): test git2 sha256 support --- .github/workflows/main.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index d9df2f20d5..65bcda2ea8 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -35,9 +35,13 @@ jobs: shell: bash - run: cargo test --locked - run: cargo test --features https,ssh + # Single-threaded: libgit2's builtin SHA256 is not thread-safe. + # See https://github.com/rust-lang/git2-rs/issues/1255. + - run: cargo test --features unstable-sha256 -- --test-threads=1 + - run: cargo test --features https,ssh,unstable-sha256 + - run: cargo test -p git2-curl - run: cargo run -p systest - run: cargo run -p systest --features unstable-sha256 - - run: cargo test -p git2-curl rustfmt: name: Rustfmt From 57a68e0729a014e92e0704c3382037f36979c13d Mon Sep 17 00:00:00 2001 From: Weihang Lo Date: Wed, 13 May 2026 22:07:27 -0400 Subject: [PATCH 09/12] feat(oid): impl Display for ObjectFormat --- src/oid.rs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/oid.rs b/src/oid.rs index 39a731a4f8..76b50d5559 100644 --- a/src/oid.rs +++ b/src/oid.rs @@ -19,6 +19,23 @@ pub enum ObjectFormat { Sha256, } +impl ObjectFormat { + /// Convert an object format to its string representation. + pub fn str(&self) -> &'static str { + match self { + ObjectFormat::Sha1 => "sha1", + #[cfg(feature = "unstable-sha256")] + ObjectFormat::Sha256 => "sha256", + } + } +} + +impl fmt::Display for ObjectFormat { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.str().fmt(f) + } +} + impl Binding for ObjectFormat { type Raw = raw::git_oid_t; From b9bde87d80eb58cc87afe22e16bd581c2bb11618 Mon Sep 17 00:00:00 2001 From: Weihang Lo Date: Thu, 14 May 2026 19:02:58 +0200 Subject: [PATCH 10/12] test: oid length sanity check --- src/oid.rs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/oid.rs b/src/oid.rs index 76b50d5559..dc0c5440bf 100644 --- a/src/oid.rs +++ b/src/oid.rs @@ -376,6 +376,18 @@ impl AsRef<[u8]> for Oid { } } +// Double-check libgit2's OID size constants to keep the docs and the +// public API surface honest. If any of these break, the doc comments on +// `Oid::as_bytes` need to be revisited. +#[cfg(not(feature = "unstable-sha256"))] +const _: () = assert!(raw::GIT_OID_MAX_SIZE == 20); +#[cfg(feature = "unstable-sha256")] +const _: () = { + assert!(raw::GIT_OID_SHA1_SIZE == 20); + assert!(raw::GIT_OID_SHA256_SIZE == 32); + assert!(raw::GIT_OID_MAX_SIZE == 32); +}; + #[cfg(test)] mod tests { use std::fs::File; From fc8af6ad26b0e1bc126d997cf30407e8561195c6 Mon Sep 17 00:00:00 2001 From: Weihang Lo Date: Thu, 14 May 2026 19:02:58 +0200 Subject: [PATCH 11/12] test(oid): split `_ext` to test each format separately So that we have a more thorough test coverage --- src/oid.rs | 228 +++++++++++++++++++++-------------------------------- 1 file changed, 88 insertions(+), 140 deletions(-) diff --git a/src/oid.rs b/src/oid.rs index dc0c5440bf..f8f357fd43 100644 --- a/src/oid.rs +++ b/src/oid.rs @@ -409,8 +409,7 @@ mod tests { } #[test] - #[cfg(feature = "unstable-sha256")] - fn conversions_object_format() { + fn conversions_ext_sha1() { use crate::ObjectFormat; assert!(Oid::from_str_ext("foo", ObjectFormat::Sha1).is_err()); @@ -420,6 +419,16 @@ mod tests { ) .is_ok()); + let sha1 = Oid::ZERO_SHA1; + assert_eq!(sha1.as_bytes().len(), raw::GIT_OID_SHA1_SIZE); + assert_eq!(sha1.to_string().len(), raw::GIT_OID_SHA1_HEXSIZE); + } + + #[test] + #[cfg(feature = "unstable-sha256")] + fn conversions_ext_sha256() { + use crate::ObjectFormat; + assert!(Oid::from_str_ext("foo", ObjectFormat::Sha256).is_err()); assert!(Oid::from_str_ext( "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", @@ -427,28 +436,12 @@ mod tests { ) .is_ok()); - assert!(Oid::from_bytes(b"foo").is_err()); - - let sha1_from_bytes = Oid::ZERO_SHA1; - let sha256_from_bytes = Oid::ZERO_SHA256; - - // as_bytes() returns logical length per OID type - assert_eq!(sha1_from_bytes.as_bytes().len(), raw::GIT_OID_SHA1_SIZE); - assert_eq!(sha256_from_bytes.as_bytes().len(), raw::GIT_OID_SHA256_SIZE); - - // raw_bytes() always returns the full buffer - assert_eq!(sha1_from_bytes.raw_bytes().len(), raw::GIT_OID_MAX_SIZE); - assert_eq!(sha256_from_bytes.raw_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 - ); + let sha256 = Oid::ZERO_SHA256; + assert_eq!(sha256.as_bytes().len(), raw::GIT_OID_SHA256_SIZE); + assert_eq!(sha256.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); + // SHA1 zero and SHA256 zero are not equal despite both being all zeros. + assert_ne!(Oid::ZERO_SHA1, sha256); } #[test] @@ -458,33 +451,33 @@ mod tests { } #[test] - #[cfg(feature = "unstable-sha256")] - fn object_format_from_oid() { + fn object_format_from_oid_ext_sha1() { use crate::ObjectFormat; - let sha1 = Oid::ZERO_SHA1; - assert_eq!(sha1.object_format(), ObjectFormat::Sha1); - - let sha256 = Oid::ZERO_SHA256; - assert_eq!(sha256.object_format(), ObjectFormat::Sha256); - - let sha1_from_str = Oid::from_str_ext( + let sha1 = Oid::from_str_ext( "decbf2be529ab6557d5429922251e5ee36519817", ObjectFormat::Sha1, ) .unwrap(); - assert_eq!(sha1_from_str.object_format(), ObjectFormat::Sha1); + assert_eq!(sha1.object_format(), ObjectFormat::Sha1); + } - let sha256_from_str = Oid::from_str_ext( + #[test] + #[cfg(feature = "unstable-sha256")] + fn object_format_from_oid_ext_sha256() { + use crate::ObjectFormat; + + assert_eq!(Oid::ZERO_SHA256.object_format(), ObjectFormat::Sha256); + + let sha256 = Oid::from_str_ext( "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", ObjectFormat::Sha256, ) .unwrap(); - assert_eq!(sha256_from_str.object_format(), ObjectFormat::Sha256); + assert_eq!(sha256.object_format(), ObjectFormat::Sha256); } #[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")?); @@ -510,85 +503,35 @@ mod tests { Ok(()) } + #[test] + fn comparisons_ext_sha1() -> Result<(), Error> { + use crate::ObjectFormat; + + let a = Oid::from_str_ext("decbf2b", ObjectFormat::Sha1)?; + let b = Oid::from_str_ext( + "decbf2b000000000000000000000000000000000", + ObjectFormat::Sha1, + )?; + assert_eq!(a, b); + assert!(a <= b); + assert!(a >= b); + assert!(Oid::from_bytes(b"00000000000000000000")? < a); + Ok(()) + } + #[test] #[cfg(feature = "unstable-sha256")] - fn comparisons_object_format() -> Result<(), Error> { + fn comparisons_ext_sha256() -> Result<(), Error> { use crate::ObjectFormat; - // SHA1 OID comparisons with explicit format - assert_eq!( - Oid::from_str_ext("decbf2b", ObjectFormat::Sha1)?, - Oid::from_str_ext("decbf2b", ObjectFormat::Sha1)? - ); - assert!( - Oid::from_str_ext("decbf2b", ObjectFormat::Sha1)? - <= Oid::from_str_ext("decbf2b", ObjectFormat::Sha1)? - ); - assert!( - Oid::from_str_ext("decbf2b", ObjectFormat::Sha1)? - >= Oid::from_str_ext("decbf2b", ObjectFormat::Sha1)? - ); - { - let o = Oid::from_str_ext("decbf2b", ObjectFormat::Sha1)?; - assert_eq!(o, o); - assert!(o <= o); - assert!(o >= o); - } - assert_eq!( - Oid::from_str_ext("decbf2b", ObjectFormat::Sha1)?, - Oid::from_str_ext( - "decbf2b000000000000000000000000000000000", - ObjectFormat::Sha1 - )? - ); + const HEX: &str = "abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890"; - // 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_ext("decbf2b", ObjectFormat::Sha1)? - ); + let a = Oid::from_str_ext(HEX, ObjectFormat::Sha256)?; + let b = Oid::from_str_ext(HEX, ObjectFormat::Sha256)?; + assert_eq!(a, b); + assert!(a <= b); + assert!(a >= b); - // SHA256 OID comparisons with explicit format (using full 64-char hex strings) - assert_eq!( - Oid::from_str_ext( - "abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890", - ObjectFormat::Sha256 - )?, - Oid::from_str_ext( - "abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890", - ObjectFormat::Sha256 - )? - ); - assert!( - Oid::from_str_ext( - "abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890", - ObjectFormat::Sha256 - )? <= Oid::from_str_ext( - "abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890", - ObjectFormat::Sha256 - )? - ); - assert!( - Oid::from_str_ext( - "abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890", - ObjectFormat::Sha256 - )? >= Oid::from_str_ext( - "abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890", - ObjectFormat::Sha256 - )? - ); - { - let o = Oid::from_str_ext( - "abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890", - ObjectFormat::Sha256, - )?; - assert_eq!(o, o); - assert!(o <= o); - assert!(o >= o); - } assert_eq!( Oid::from_str_ext("abcdef12", ObjectFormat::Sha256)?, Oid::from_str_ext( @@ -602,14 +545,7 @@ mod tests { Oid::from_bytes(b"00000000000000000000000000000000")? < Oid::from_bytes(b"00000000000000000000000000000001")? ); - assert!( - Oid::from_bytes(b"00000000000000000000000000000000")? - < Oid::from_str_ext( - "abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890", - ObjectFormat::Sha256 - )? - ); - + assert!(Oid::from_bytes(b"00000000000000000000000000000000")? < a); Ok(()) } @@ -638,23 +574,26 @@ mod tests { } #[test] - #[cfg(feature = "unstable-sha256")] - fn hash_object_with_format() -> Result<(), Error> { + fn hash_object_ext_sha1() -> Result<(), Error> { use crate::ObjectFormat; - let bytes = b"hello world"; - - let sha1_oid = Oid::hash_object_ext(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_SHA1_SIZE); - - let sha256_oid = Oid::hash_object_ext(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_SHA256_SIZE); + let oid = Oid::hash_object_ext(ObjectType::Blob, b"hello world", ObjectFormat::Sha1)?; + assert_eq!(oid.to_string().len(), raw::GIT_OID_SHA1_HEXSIZE); + assert_eq!(oid.as_bytes().len(), raw::GIT_OID_SHA1_SIZE); + Ok(()) + } - // Different formats produce different OIDs - assert_ne!(sha1_oid, sha256_oid); + #[test] + #[cfg(feature = "unstable-sha256")] + fn hash_object_ext_sha256() -> Result<(), Error> { + use crate::ObjectFormat; + let bytes = b"hello world"; + let sha1 = Oid::hash_object_ext(ObjectType::Blob, bytes, ObjectFormat::Sha1)?; + let sha256 = Oid::hash_object_ext(ObjectType::Blob, bytes, ObjectFormat::Sha256)?; + assert_eq!(sha256.to_string().len(), raw::GIT_OID_SHA256_HEXSIZE); + assert_eq!(sha256.as_bytes().len(), raw::GIT_OID_SHA256_SIZE); + assert_ne!(sha1, sha256); Ok(()) } @@ -670,8 +609,7 @@ mod tests { } #[test] - #[cfg(feature = "unstable-sha256")] - fn hash_file_object_format() -> Result<(), Error> { + fn hash_file_ext_sha1() -> Result<(), Error> { use crate::ObjectFormat; let td = TempDir::new().unwrap(); @@ -679,17 +617,27 @@ mod tests { let mut file = File::create(&path).unwrap(); file.write_all(b"test content").unwrap(); - let sha1_oid = Oid::hash_file_ext(ObjectType::Blob, &path, ObjectFormat::Sha1)?; - assert_eq!(sha1_oid.to_string().len(), raw::GIT_OID_SHA1_HEXSIZE); - assert_eq!(sha1_oid.as_bytes().len(), raw::GIT_OID_SHA1_SIZE); + let oid = Oid::hash_file_ext(ObjectType::Blob, &path, ObjectFormat::Sha1)?; + assert_eq!(oid.to_string().len(), raw::GIT_OID_SHA1_HEXSIZE); + assert_eq!(oid.as_bytes().len(), raw::GIT_OID_SHA1_SIZE); + Ok(()) + } - let sha256_oid = Oid::hash_file_ext(ObjectType::Blob, &path, ObjectFormat::Sha256)?; - assert_eq!(sha256_oid.to_string().len(), raw::GIT_OID_SHA256_HEXSIZE); - assert_eq!(sha256_oid.as_bytes().len(), raw::GIT_OID_SHA256_SIZE); + #[test] + #[cfg(feature = "unstable-sha256")] + fn hash_file_ext_sha256() -> Result<(), Error> { + use crate::ObjectFormat; - // Different formats produce different OIDs - assert_ne!(sha1_oid, sha256_oid); + 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::hash_file_ext(ObjectType::Blob, &path, ObjectFormat::Sha1)?; + let sha256 = Oid::hash_file_ext(ObjectType::Blob, &path, ObjectFormat::Sha256)?; + assert_eq!(sha256.to_string().len(), raw::GIT_OID_SHA256_HEXSIZE); + assert_eq!(sha256.as_bytes().len(), raw::GIT_OID_SHA256_SIZE); + assert_ne!(sha1, sha256); Ok(()) } } From 7bd145ee23884f19d1d79ce0e46c1ab105784040 Mon Sep 17 00:00:00 2001 From: Weihang Lo Date: Sun, 17 May 2026 20:20:59 +0200 Subject: [PATCH 12/12] fix: import `ObjectFormat` in one place --- src/oid.rs | 30 ++++-------------------------- 1 file changed, 4 insertions(+), 26 deletions(-) diff --git a/src/oid.rs b/src/oid.rs index f8f357fd43..bac03d36bf 100644 --- a/src/oid.rs +++ b/src/oid.rs @@ -397,6 +397,7 @@ mod tests { use super::Error; use super::Oid; + use crate::ObjectFormat; use crate::ObjectType; use tempfile::TempDir; @@ -410,8 +411,6 @@ mod tests { #[test] fn conversions_ext_sha1() { - use crate::ObjectFormat; - assert!(Oid::from_str_ext("foo", ObjectFormat::Sha1).is_err()); assert!(Oid::from_str_ext( "decbf2be529ab6557d5429922251e5ee36519817", @@ -427,8 +426,6 @@ mod tests { #[test] #[cfg(feature = "unstable-sha256")] fn conversions_ext_sha256() { - use crate::ObjectFormat; - assert!(Oid::from_str_ext("foo", ObjectFormat::Sha256).is_err()); assert!(Oid::from_str_ext( "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", @@ -447,13 +444,11 @@ mod tests { #[test] fn object_format_always_sha1() { let oid = Oid::ZERO_SHA1; - assert_eq!(oid.object_format(), crate::ObjectFormat::Sha1); + assert_eq!(oid.object_format(), ObjectFormat::Sha1); } #[test] fn object_format_from_oid_ext_sha1() { - use crate::ObjectFormat; - let sha1 = Oid::from_str_ext( "decbf2be529ab6557d5429922251e5ee36519817", ObjectFormat::Sha1, @@ -465,8 +460,6 @@ mod tests { #[test] #[cfg(feature = "unstable-sha256")] fn object_format_from_oid_ext_sha256() { - use crate::ObjectFormat; - assert_eq!(Oid::ZERO_SHA256.object_format(), ObjectFormat::Sha256); let sha256 = Oid::from_str_ext( @@ -505,8 +498,6 @@ mod tests { #[test] fn comparisons_ext_sha1() -> Result<(), Error> { - use crate::ObjectFormat; - let a = Oid::from_str_ext("decbf2b", ObjectFormat::Sha1)?; let b = Oid::from_str_ext( "decbf2b000000000000000000000000000000000", @@ -522,8 +513,6 @@ mod tests { #[test] #[cfg(feature = "unstable-sha256")] fn comparisons_ext_sha256() -> Result<(), Error> { - use crate::ObjectFormat; - const HEX: &str = "abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890"; let a = Oid::from_str_ext(HEX, ObjectFormat::Sha256)?; @@ -552,17 +541,14 @@ mod tests { #[test] fn zero_is_zero() { assert!(Oid::ZERO_SHA1.is_zero()); - assert_eq!(Oid::ZERO_SHA1.object_format(), crate::ObjectFormat::Sha1); + assert_eq!(Oid::ZERO_SHA1.object_format(), ObjectFormat::Sha1); } #[test] #[cfg(feature = "unstable-sha256")] fn zero_sha256_is_zero() { assert!(Oid::ZERO_SHA256.is_zero()); - assert_eq!( - Oid::ZERO_SHA256.object_format(), - crate::ObjectFormat::Sha256 - ); + assert_eq!(Oid::ZERO_SHA256.object_format(), ObjectFormat::Sha256); } #[test] @@ -575,8 +561,6 @@ mod tests { #[test] fn hash_object_ext_sha1() -> Result<(), Error> { - use crate::ObjectFormat; - let oid = Oid::hash_object_ext(ObjectType::Blob, b"hello world", ObjectFormat::Sha1)?; assert_eq!(oid.to_string().len(), raw::GIT_OID_SHA1_HEXSIZE); assert_eq!(oid.as_bytes().len(), raw::GIT_OID_SHA1_SIZE); @@ -586,8 +570,6 @@ mod tests { #[test] #[cfg(feature = "unstable-sha256")] fn hash_object_ext_sha256() -> Result<(), Error> { - use crate::ObjectFormat; - let bytes = b"hello world"; let sha1 = Oid::hash_object_ext(ObjectType::Blob, bytes, ObjectFormat::Sha1)?; let sha256 = Oid::hash_object_ext(ObjectType::Blob, bytes, ObjectFormat::Sha256)?; @@ -610,8 +592,6 @@ mod tests { #[test] fn hash_file_ext_sha1() -> Result<(), Error> { - use crate::ObjectFormat; - let td = TempDir::new().unwrap(); let path = td.path().join("test.txt"); let mut file = File::create(&path).unwrap(); @@ -626,8 +606,6 @@ mod tests { #[test] #[cfg(feature = "unstable-sha256")] fn hash_file_ext_sha256() -> Result<(), Error> { - use crate::ObjectFormat; - let td = TempDir::new().unwrap(); let path = td.path().join("test.txt"); let mut file = File::create(&path).unwrap();