From 738ee1a844310a210fbac9c9b0653fbad1321f8d Mon Sep 17 00:00:00 2001 From: Keno Fischer Date: Sun, 28 Sep 2025 21:37:25 +0000 Subject: [PATCH] LibGit2: Add wrappers for git_apply_to_tree, git_diff_from_buffer, and git_index_write_tree_to Added Julia wrappers for three libgit2 functions with idiomatic APIs: - `GitDiff(::AbstractString)`: Parse diffs from unified diff format buffers - `apply_to_tree(repo, preimage, diff, options)`: Apply a diff to a tree, returning a GitIndex with the result - `write_tree_to!(repo, idx)`: Write an index as a tree to a repository Made GitDiff and GitDiffStats repository-independent since they can be created from buffers and all operations work purely on object pointers. Added ApplyOptions struct for apply configuration and tests for all new functionality. --- stdlib/LibGit2/src/diff.jl | 37 +++++++++++++++++++++++++--- stdlib/LibGit2/src/index.jl | 26 +++++++++++++++++++ stdlib/LibGit2/src/tree.jl | 32 ++++++++++++++++++++++++ stdlib/LibGit2/src/types.jl | 26 +++++++++++++++++-- stdlib/LibGit2/test/libgit2-tests.jl | 14 +++++++++++ 5 files changed, 130 insertions(+), 5 deletions(-) diff --git a/stdlib/LibGit2/src/diff.jl b/stdlib/LibGit2/src/diff.jl index 3162d604acd36..0f69c10f6b053 100644 --- a/stdlib/LibGit2/src/diff.jl +++ b/stdlib/LibGit2/src/diff.jl @@ -35,7 +35,7 @@ function diff_tree(repo::GitRepo, tree::GitTree, pathspecs::AbstractString=""; c (Ptr{Ptr{Cvoid}}, Ptr{Cvoid}, Ptr{Cvoid}, Ptr{DiffOptionsStruct}), diff_ptr_ptr, repo, tree, isempty(pathspecs) ? C_NULL : pathspecs) end - return GitDiff(repo, diff_ptr_ptr[]) + return GitDiff(diff_ptr_ptr[]) end """ @@ -54,7 +54,7 @@ function diff_tree(repo::GitRepo, oldtree::GitTree, newtree::GitTree) @check ccall((:git_diff_tree_to_tree, libgit2), Cint, (Ptr{Ptr{Cvoid}}, Ptr{Cvoid}, Ptr{Cvoid}, Ptr{Cvoid}, Ptr{DiffOptionsStruct}), diff_ptr_ptr, repo, oldtree, newtree, C_NULL) - return GitDiff(repo, diff_ptr_ptr[]) + return GitDiff(diff_ptr_ptr[]) end """ @@ -70,7 +70,7 @@ function GitDiffStats(diff::GitDiff) @check ccall((:git_diff_get_stats, libgit2), Cint, (Ptr{Ptr{Cvoid}}, Ptr{Cvoid}), diff_stat_ptr_ptr, diff) - return GitDiffStats(diff.owner, diff_stat_ptr_ptr[]) + return GitDiffStats(diff_stat_ptr_ptr[]) end """ @@ -142,3 +142,34 @@ function Base.show(io::IO, diff::GitDiff) println(io, "Number of deltas: $(count(diff))") show(io, GitDiffStats(diff)) end + +""" + GitDiff(content::AbstractString) + +Parse a diff from a buffer. The `content` should be in unified diff format. +Returns a [`GitDiff`](@ref) object. + +This is equivalent to [`git_diff_from_buffer`](https://libgit2.org/libgit2/#HEAD/group/diff/git_diff_from_buffer). + +# Examples +```julia +diff_str = \"\"\" +diff --git a/file.txt b/file.txt +index 1234567..abcdefg 100644 +--- a/file.txt ++++ b/file.txt +@@ -1 +1 @@ +-old content ++new content +\"\"\" +diff = LibGit2.GitDiff(diff_str) +``` +""" +function GitDiff(content::AbstractString) + ensure_initialized() + diff_ptr_ptr = Ref{Ptr{Cvoid}}(C_NULL) + @check ccall((:git_diff_from_buffer, libgit2), Cint, + (Ptr{Ptr{Cvoid}}, Cstring, Csize_t), + diff_ptr_ptr, content, sizeof(content)) + return GitDiff(diff_ptr_ptr[]) +end diff --git a/stdlib/LibGit2/src/index.jl b/stdlib/LibGit2/src/index.jl index f84d0424f421f..c567e603404ea 100644 --- a/stdlib/LibGit2/src/index.jl +++ b/stdlib/LibGit2/src/index.jl @@ -56,6 +56,32 @@ function write_tree!(idx::GitIndex) return oid_ptr[] end +""" + write_tree_to!(repo::GitRepo, idx::GitIndex)::GitHash + +Write the index `idx` as a [`GitTree`](@ref) to the given repository `repo`. +This is similar to [`write_tree!`](@ref) but allows writing the index to a +different repository than the one it may be associated with. + +Trees will be recursively created for each subtree in `idx`. The returned +[`GitHash`](@ref) can be used to create a [`GitCommit`](@ref). + +This is equivalent to [`git_index_write_tree_to`](https://libgit2.org/libgit2/#HEAD/group/index/git_index_write_tree_to). + +# Examples +```julia +idx = LibGit2.GitIndex(source_repo) +tree_oid = LibGit2.write_tree_to!(target_repo, idx) +``` +""" +function write_tree_to!(repo::GitRepo, idx::GitIndex) + ensure_initialized() + oid_ptr = Ref(GitHash()) + @check ccall((:git_index_write_tree_to, libgit2), Cint, + (Ptr{GitHash}, Ptr{Cvoid}, Ptr{Cvoid}), oid_ptr, idx, repo) + return oid_ptr[] +end + function repository(idx::GitIndex) if idx.owner === nothing throw(GitError(Error.Index, Error.ENOTFOUND, "Index does not have an owning repository.")) diff --git a/stdlib/LibGit2/src/tree.jl b/stdlib/LibGit2/src/tree.jl index eccae70de872b..4de63d62b24d5 100644 --- a/stdlib/LibGit2/src/tree.jl +++ b/stdlib/LibGit2/src/tree.jl @@ -194,3 +194,35 @@ end function Base.haskey(tree::GitTree, target::AbstractString) return _getindex(tree, target) !== nothing end + +""" + apply_to_tree(repo::GitRepo, preimage::GitTree, diff::GitDiff, options::ApplyOptions=ApplyOptions()) + +Apply a [`GitDiff`](@ref) to a [`GitTree`](@ref), returning the resulting index. +The `preimage` is the tree to which the diff will be applied. The `diff` should be +generated from the `preimage` to some other tree. + +The returned [`GitIndex`](@ref) contains the result of applying the diff and can be +written as a tree using [`write_tree_to!`](@ref). + +This is equivalent to [`git_apply_to_tree`](https://libgit2.org/libgit2/#HEAD/group/apply/git_apply_to_tree). + +# Examples +```julia +repo = LibGit2.GitRepo(repo_path) +tree1 = LibGit2.GitTree(repo, "HEAD^{tree}") +tree2 = LibGit2.GitTree(repo, "HEAD~1^{tree}") +diff = LibGit2.diff_tree(repo, tree1, tree2) +result_index = LibGit2.apply_to_tree(repo, tree1, diff) +tree_oid = LibGit2.write_tree_to!(repo, result_index) +``` +""" +function apply_to_tree(repo::GitRepo, preimage::GitTree, diff::GitDiff, options::ApplyOptions=ApplyOptions()) + ensure_initialized() + out_index_ptr = Ref{Ptr{Cvoid}}(C_NULL) + opts_ptr = Ref(options) + @check ccall((:git_apply_to_tree, libgit2), Cint, + (Ptr{Ptr{Cvoid}}, Ptr{Cvoid}, Ptr{Cvoid}, Ptr{Cvoid}, Ptr{ApplyOptions}), + out_index_ptr, repo, preimage, diff, opts_ptr) + return GitIndex(nothing, out_index_ptr[]) +end diff --git a/stdlib/LibGit2/src/types.jl b/stdlib/LibGit2/src/types.jl index 17f13b68da362..ad8c54f16d16c 100644 --- a/stdlib/LibGit2/src/types.jl +++ b/stdlib/LibGit2/src/types.jl @@ -859,6 +859,28 @@ The fields represent: end @assert Base.allocatedinline(StatusOptions) +""" + LibGit2.ApplyOptions + +Options for applying a diff. +Matches the [`git_apply_options`](https://libgit2.org/libgit2/#HEAD/type/git_apply_options) struct. + +The fields represent: + * `version`: version of the struct in use, in case this changes later. For now, always `1`. + * `delta_cb`: optional callback that will be made before each delta is applied. + * `hunk_cb`: optional callback that will be made before each hunk is applied. + * `payload`: the payload for the callback functions. + * `flags`: flags controlling how the apply is performed (e.g., check mode). +""" +@kwdef struct ApplyOptions + version::Cuint = Cuint(1) + delta_cb::Ptr{Cvoid} = C_NULL + hunk_cb::Ptr{Cvoid} = C_NULL + payload::Any = nothing + flags::Cuint = Cuint(0) +end +@assert Base.allocatedinline(ApplyOptions) + """ LibGit2.StatusEntry @@ -1042,8 +1064,8 @@ for (typ, owntyp, sup, cname) in Tuple{Symbol,Any,Symbol,Symbol}[ (:GitRevWalker, :GitRepo, :AbstractGitObject, :git_revwalk), (:GitReference, :GitRepo, :AbstractGitObject, :git_reference), (:GitDescribeResult, :GitRepo, :AbstractGitObject, :git_describe_result), - (:GitDiff, :GitRepo, :AbstractGitObject, :git_diff), - (:GitDiffStats, :GitRepo, :AbstractGitObject, :git_diff_stats), + (:GitDiff, nothing, :AbstractGitObject, :git_diff), + (:GitDiffStats, nothing, :AbstractGitObject, :git_diff_stats), (:GitAnnotated, :GitRepo, :AbstractGitObject, :git_annotated_commit), (:GitRebase, :GitRepo, :AbstractGitObject, :git_rebase), (:GitBlame, :GitRepo, :AbstractGitObject, :git_blame), diff --git a/stdlib/LibGit2/test/libgit2-tests.jl b/stdlib/LibGit2/test/libgit2-tests.jl index c74734ccd42f9..4c0099f7296f4 100644 --- a/stdlib/LibGit2/test/libgit2-tests.jl +++ b/stdlib/LibGit2/test/libgit2-tests.jl @@ -1147,6 +1147,20 @@ mktempdir() do dir @test !LibGit2.isdirty(repo, cached=true) @test !LibGit2.isdiff(repo, "HEAD", cached=true) end + + LibGit2.with(LibGit2.GitRepo(cache_repo)) do repo + diff_str = "diff --git a/test.txt b/test.txt\nindex 0000000..1111111 100644\n" + @test_throws LibGit2.GitError LibGit2.GitDiff(diff_str) + + tree1 = LibGit2.GitTree(repo, "HEAD~1^{tree}") + tree2 = LibGit2.GitTree(repo, "HEAD^{tree}") + diff = LibGit2.diff_tree(repo, tree1, tree2) + idx = LibGit2.apply_to_tree(repo, tree1, diff) + @test idx isa LibGit2.GitIndex + oid = LibGit2.write_tree_to!(repo, idx) + @test oid isa LibGit2.GitHash + close(idx) + end end end