Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,13 @@ jobs:
shell: bash
- run: cargo test --locked
Copy link
Copy Markdown
Contributor

@ehuss ehuss May 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moving these breaks the --locked check since the earlier cargo run will update the lockfile. Can you either move them back, or add --locked to the other commands?

View changes since the review

- 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
Expand Down
16 changes: 16 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Expand Down
2 changes: 1 addition & 1 deletion examples/diff.rs
Original file line number Diff line number Diff line change
Expand Up @@ -319,7 +319,7 @@ fn tree_to_treeish<'a>(

fn resolve_blob<'a>(repo: &'a Repository, arg: Option<&String>) -> Result<Option<Blob<'a>>, 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))
Expand Down
20 changes: 20 additions & 0 deletions examples/init.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
#![deny(warnings)]

use clap::Parser;
use git2::ObjectFormat;
use git2::{Error, Repository, RepositoryInitMode, RepositoryInitOptions};
use std::path::{Path, PathBuf};

Expand All @@ -40,6 +41,9 @@ struct Args {
#[structopt(name = "perms", long = "shared")]
/// permissions to create the repository with
flag_shared: Option<String>,
#[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<ObjectFormat>,
}

fn run(args: &Args) -> Result<(), Error> {
Expand All @@ -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 {
Expand All @@ -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)?
};

Expand Down Expand Up @@ -136,6 +147,15 @@ fn parse_shared(shared: &str) -> Result<RepositoryInitMode, Error> {
}
}

fn parse_object_format(format: &str) -> Result<ObjectFormat, Error> {
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) {
Expand Down
46 changes: 46 additions & 0 deletions src/commit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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"));
Expand All @@ -468,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());
}
}
70 changes: 63 additions & 7 deletions src/diff.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};

Expand Down Expand Up @@ -310,16 +311,36 @@ 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<Diff<'static>, 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<Diff<'static>, 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();
// NOTE: Doesn't depend on repo, so lifetime can be 'static
unsafe {
// NOTE: Doesn't depend on repo, so lifetime can be 'static
Comment thread
weihanglo marked this conversation as resolved.
try_call!(raw::git_diff_from_buffer(
&mut diff,
buffer.as_ptr() as *const c_char,
buffer.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))
}
}
Expand Down Expand Up @@ -1552,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;
Expand All @@ -1568,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]
Expand Down Expand Up @@ -1858,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);
}
}
Loading
Loading