diff --git a/base/libgit2/blame.jl b/base/libgit2/blame.jl new file mode 100644 index 00000000000000..b24684b3f002d6 --- /dev/null +++ b/base/libgit2/blame.jl @@ -0,0 +1,37 @@ +# This file is a part of Julia. License is MIT: https://julialang.org/license + +function GitBlame(repo::GitRepo, path::AbstractString; options::BlameOptions=BlameOptions()) + blame_ptr_ptr = Ref{Ptr{Void}}(C_NULL) + @check ccall((:git_blame_file, :libgit2), Cint, + (Ptr{Ptr{Void}}, Ptr{Void}, Cstring, Ptr{BlameOptions}), + blame_ptr_ptr, repo.ptr, path, Ref(options)) + return GitBlame(repo, blame_ptr_ptr[]) +end + +function counthunks(blame::GitBlame) + return ccall((:git_blame_get_hunk_count, :libgit2), Int32, (Ptr{Void},), blame.ptr) +end + +function Base.getindex(blame::GitBlame, i::Integer) + if !(1 <= i <= counthunks(blame)) + throw(BoundsError(blame, (i,))) + end + hunk_ptr = ccall((:git_blame_get_hunk_byindex, :libgit2), + Ptr{BlameHunk}, + (Ptr{Void}, Csize_t), blame.ptr, i-1) + return unsafe_load(hunk_ptr) +end + +function Base.show(io::IO, blame_hunk::BlameHunk) + println(io, "GitBlameHunk:") + println(io, "Original path: ", blame_hunk.orig.path) + println(io, "Lines in hunk: ", blame_hunk.lines_in_hunk) + println(io, "Final commit oid: ", blame_hunk.final_commit_id) + print(io, "Final signature: ") + show(io, blame_hunk.final_signature) + println() + println(io, "Original commit oid: ", blame_hunk.orig_commit_id) + print(io, "Original signature: ") + show(io, blame_hunk.orig_signature) + println() +end diff --git a/base/libgit2/libgit2.jl b/base/libgit2/libgit2.jl index 554463406f68e5..bd11cde00acf76 100644 --- a/base/libgit2/libgit2.jl +++ b/base/libgit2/libgit2.jl @@ -30,6 +30,7 @@ include("tag.jl") include("blob.jl") include("diff.jl") include("rebase.jl") +include("blame.jl") include("status.jl") include("tree.jl") include("callbacks.jl") diff --git a/base/libgit2/types.jl b/base/libgit2/types.jl index c51e74ecc4dcce..254ce83b0eb0e7 100644 --- a/base/libgit2/types.jl +++ b/base/libgit2/types.jl @@ -374,6 +374,21 @@ Matches the [`git_merge_options`](https://libgit2.github.com/libgit2/#HEAD/type/ file_flags::GIT_MERGE_FILE = Consts.MERGE_FILE_DEFAULT end +""" + LibGit2.BlameOptions + +Matches the [`git_blame_options`](https://libgit2.github.com/libgit2/#HEAD/type/git_blame_options) struct. +""" +@kwdef struct BlameOptions + version::Cuint = 1 + flags::UInt32 = 0 + min_match_characters::UInt16 = 20 + newest_commit::GitHash + oldest_commit::GitHash + min_line::Csize_t = 1 + max_line::Csize_t = 0 +end + """ LibGit2.PushOptions @@ -562,6 +577,7 @@ for (typ, owntyp, sup, cname) in [ (:GitDiffStats, :GitRepo, :AbstractGitObject, :git_diff_stats), (:GitAnnotated, :GitRepo, :AbstractGitObject, :git_annotated_commit), (:GitRebase, :GitRepo, :AbstractGitObject, :git_rebase), + (:GitBlame, :GitRepo, :AbstractGitObject, :git_blame), (:GitStatus, :GitRepo, :AbstractGitObject, :git_status_list), (:GitBranchIter, :GitRepo, :AbstractGitObject, :git_branch_iterator), (:GitConfigIter, nothing, :AbstractGitObject, :git_config_iterator), @@ -658,6 +674,43 @@ mutable struct Signature time_offset::Cint end +""" + LibGit2.BlameHunk + +Matches the [`git_blame_hunk`](https://libgit2.github.com/libgit2/#HEAD/type/git_blame_hunk) struct. +The fields represent: + * `lines_in_hunk`: the number of lines in this hunk of the blame. + * `final_commit_id`: the [`GitHash`](@ref) of the commit where this section was last changed. + * `final_start_line_number`: the *one based* line number in the file where the + hunk starts, in the *final* version of the file. + * `final_signature`: the signature of the person who last modified this hunk. You will + need to pass this to [`Signature`](@ref) to access its fields. + * `orig_commit_id`: the [`GitHash`](@ref) of the commit where this hunk was first found. + * `orig_path`: the path to the file where the hunk originated. This may be different + than the current/final path, for instance if the file has been moved. + * `orig_start_line_number`: the *one based* line number in the file where the + hunk starts, in the *original* version of the file at `orig_path`. + * `orig_signature`: the signature of the person who introduced this hunk. You will + need to pass this to [`Signature`](@ref) to access its fields. + * `boundary`: `'1'` if the original commit is a "boundary" commit (for instance, if it's + equal to an oldest commit set in `options`). +""" +@kwdef struct BlameHunk + lines_in_hunk::Csize_t + + final_commit_id::GitHash + final_start_line_number::Csize_t + final_signature::Ptr{SignatureStruct} + + orig_commit_id::GitHash + orig_path::Cstring + orig_start_line_number::Csize_t + orig_signature::Ptr{SignatureStruct} + + boundary::Char +end + + """ Resource management helper function """ function with(f::Function, obj) diff --git a/test/libgit2.jl b/test/libgit2.jl index 4ef1ea6448f1c7..c98fb43b637098 100644 --- a/test/libgit2.jl +++ b/test/libgit2.jl @@ -509,6 +509,14 @@ mktempdir() do dir @test showstr[5] == "Message:" @test showstr[6] == commit_msg1 @test LibGit2.revcount(repo, string(commit_oid1), string(commit_oid3)) == (-1,0) + + blame = LibGit2.GitBlame(repo, test_file) + @test LibGit2.counthunks(blame) == 3 + @test_throws BoundsError getindex(blame, LibGit2.counthunks(blame)+1) + @test_throws BoundsError getindex(blame, 0) + sig = LibGit2.Signature(blame[1].orig_signature) + @test sig.name == cmtr.name + @test sig.email == cmtr.email finally close(cmt) end