Skip to content
Open
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
5 changes: 3 additions & 2 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,11 @@ jobs:
- name: Install Rust (rustup)
run: rustup update ${{ matrix.rust }} --no-self-update && rustup default ${{ matrix.rust }}
shell: bash
- run: cargo test --locked
- run: cargo test --features https,ssh
- run: cargo run -p systest
- run: cargo run -p systest --features unstable-sha256
- 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
- run: cargo test --features unstable-sha256
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.

Can we also make sure this covers networking (and examples)? Maybe something like:

Suggested change
- run: cargo test --features unstable-sha256
- run: cargo test --features https,ssh,unstable-sha256

View changes since the review

- run: cargo test -p git2-curl

rustfmt:
Expand Down
12 changes: 12 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,18 @@ url = "2.5.4"

[features]
unstable = []
# Experimental SHA256 OID support,
# reflecting upstream libgit2's GIT_EXPERIMENTAL_SHA256.
#
# This is an ABI-breaking change.
# Future releases with this feature may introduce breakages without notice
# Use at your own risk.
#
# Library authors:
# DO NOT enable this feature by default in your dependencies.
# Due to Cargo's additive features,
# downstream users cannot deactivate it once enabled.
unstable-sha256 = ["libgit2-sys/unstable-sha256"]
default = []
ssh = ["libgit2-sys/ssh", "cred"]
https = ["libgit2-sys/https", "openssl-sys", "openssl-probe", "cred"]
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)
Copy link
Copy Markdown
Contributor

@DanielEScherzer DanielEScherzer May 9, 2026

Choose a reason for hiding this comment

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

Suggested change
/// object format to use (sha1 or sha256, requires unstable-sha256 feature)
/// object format to use (sha1 or sha256, requires unstable-sha256 feature to use the sha256 format)

View changes since the review

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
50 changes: 46 additions & 4 deletions src/commit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -437,10 +437,11 @@ mod tests {
assert_eq!(commit.parents().count(), 0);

let tree_header_bytes = commit.header_field_bytes("tree").unwrap();
assert_eq!(
crate::Oid::from_str(tree_header_bytes.as_str().unwrap()).unwrap(),
commit.tree_id()
);
let tree_oid = {
let str = tree_header_bytes.as_str().unwrap();
crate::Oid::from_str_ext(str, repo.object_format()).unwrap()
};
assert_eq!(tree_oid, commit.tree_id());
assert_eq!(commit.author().name(), Some("name"));
assert_eq!(commit.author().email(), Some("email"));
assert_eq!(commit.committer().name(), Some("name"));
Expand All @@ -467,4 +468,45 @@ mod tests {
.ok()
.unwrap();
}

#[test]
#[cfg(feature = "unstable-sha256")]
fn smoke_sha256() {
let (_td, repo) = crate::test::repo_init_sha256();
let head = repo.head().unwrap();
let target = head.target().unwrap();
let commit = repo.find_commit(target).unwrap();

// Verify SHA256 OID (32 bytes)
assert_eq!(commit.id().as_bytes().len(), 32);
assert_eq!(commit.tree_id().as_bytes().len(), 32);

assert_eq!(commit.message(), Some("initial\n\nbody"));
assert_eq!(commit.body(), Some("body"));
assert_eq!(commit.id(), target);
commit.summary().unwrap();
commit.tree().unwrap();
assert_eq!(commit.parents().count(), 0);

let tree_header_bytes = commit.header_field_bytes("tree").unwrap();
let tree_oid = {
let str = tree_header_bytes.as_str().unwrap();
let oid = crate::Oid::from_str_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());
}
}
71 changes: 64 additions & 7 deletions src/diff.rs
Original file line number Diff line number Diff line change
Expand Up @@ -310,16 +310,38 @@ 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, crate::ObjectFormat::Sha1)
Copy link
Copy Markdown
Contributor

@DanielEScherzer DanielEScherzer May 9, 2026

Choose a reason for hiding this comment

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

I can't make suggestions on the unmodified line, but I suggest adding an import for crate::ObjectFormat rather than needing to fully qualify it

View changes since the review

}

/// 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: crate::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();
unsafe {
// NOTE: Doesn't depend on repo, so lifetime can be 'static
Copy link
Copy Markdown
Contributor

@DanielEScherzer DanielEScherzer May 9, 2026

Choose a reason for hiding this comment

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

this explanation of why things can be 'static is lost

View changes since the review

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 +1574,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 +1592,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 +1882,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