diff --git a/blame.go b/blame.go new file mode 100644 index 0000000..713f83e --- /dev/null +++ b/blame.go @@ -0,0 +1,138 @@ +package git + +/* +#include +#include +*/ +import "C" +import "runtime" + +type BlameOptions struct { + Flags BlameOptionsFlag + MinMatchCharacters uint16 + NewestCommit *Oid + OldestCommit *Oid + MinLine uint32 + MaxLine uint32 +} + +func DefaultBlameOptions() (BlameOptions, error) { + opts := C.git_blame_options{} + ecode := C.git_blame_init_options(&opts, C.GIT_BLAME_OPTIONS_VERSION) + if ecode < 0 { + return BlameOptions{}, MakeGitError(ecode) + } + + return BlameOptions{ + Flags: BlameOptionsFlag(opts.flags), + MinMatchCharacters: uint16(opts.min_match_characters), + NewestCommit: newOidFromC(&opts.newest_commit), + OldestCommit: newOidFromC(&opts.oldest_commit), + MinLine: uint32(opts.min_line), + MaxLine: uint32(opts.max_line), + }, nil +} + +type BlameOptionsFlag uint32 + +const ( + BlameNormal BlameOptionsFlag = C.GIT_BLAME_NORMAL + BlameTrackCopiesSameFile BlameOptionsFlag = C.GIT_BLAME_TRACK_COPIES_SAME_FILE + BlameTrackCopiesSameCommitMoves BlameOptionsFlag = C.GIT_BLAME_TRACK_COPIES_SAME_COMMIT_MOVES + BlameTrackCopiesSameCommitCopies BlameOptionsFlag = C.GIT_BLAME_TRACK_COPIES_SAME_COMMIT_COPIES + BlameTrackCopiesAnyCommitCopies BlameOptionsFlag = C.GIT_BLAME_TRACK_COPIES_ANY_COMMIT_COPIES + BlameFirstParent BlameOptionsFlag = C.GIT_BLAME_FIRST_PARENT +) + +func (v *Repository) BlameFile(path string, opts *BlameOptions) (*Blame, error) { + var blamePtr *C.git_blame + + var copts *C.git_blame_options + if opts != nil { + copts = &C.git_blame_options{ + version: C.GIT_BLAME_OPTIONS_VERSION, + flags: C.uint32_t(opts.Flags), + min_match_characters: C.uint16_t(opts.MinMatchCharacters), + min_line: C.uint32_t(opts.MinLine), + max_line: C.uint32_t(opts.MaxLine), + } + if opts.NewestCommit != nil { + copts.newest_commit = *opts.NewestCommit.toC() + } + if opts.OldestCommit != nil { + copts.oldest_commit = *opts.OldestCommit.toC() + } + } + + ecode := C.git_blame_file(&blamePtr, v.ptr, C.CString(path), copts) + if ecode < 0 { + return nil, MakeGitError(ecode) + } + + return newBlameFromC(blamePtr), nil +} + +type Blame struct { + ptr *C.git_blame +} + +func (blame *Blame) HunkCount() int { + return int(C.git_blame_get_hunk_count(blame.ptr)) +} + +func (blame *Blame) HunkByIndex(index int) (BlameHunk, error) { + ptr := C.git_blame_get_hunk_byindex(blame.ptr, C.uint32_t(index)) + if ptr == nil { + return BlameHunk{}, ErrInvalid + } + return blameHunkFromC(ptr), nil +} + +func newBlameFromC(ptr *C.git_blame) *Blame { + if ptr == nil { + return nil + } + + blame := &Blame{ + ptr: ptr, + } + + runtime.SetFinalizer(blame, (*Blame).Free) + return blame +} + +func (blame *Blame) Free() error { + if blame.ptr == nil { + return ErrInvalid + } + runtime.SetFinalizer(blame, nil) + C.git_blame_free(blame.ptr) + blame.ptr = nil + return nil +} + +type BlameHunk struct { + LinesInHunk uint16 + FinalCommitId *Oid + FinalStartLineNumber uint16 + FinalSignature *Signature + OrigCommitId *Oid + OrigPath string + OrigStartLineNumber uint16 + OrigSignature *Signature + Boundary bool +} + +func blameHunkFromC(hunk *C.git_blame_hunk) BlameHunk { + return BlameHunk{ + LinesInHunk: uint16(hunk.lines_in_hunk), + FinalCommitId: newOidFromC(&hunk.final_commit_id), + FinalStartLineNumber: uint16(hunk.final_start_line_number), + FinalSignature: newSignatureFromC(hunk.final_signature), + OrigCommitId: newOidFromC(&hunk.orig_commit_id), + OrigPath: C.GoString(hunk.orig_path), + OrigStartLineNumber: uint16(hunk.orig_start_line_number), + OrigSignature: newSignatureFromC(hunk.orig_signature), + Boundary: hunk.boundary == 1, + } +} diff --git a/blame_test.go b/blame_test.go new file mode 100644 index 0000000..7c730a0 --- /dev/null +++ b/blame_test.go @@ -0,0 +1,63 @@ +package git + +import ( + "os" + "reflect" + "testing" +) + +func TestBlame(t *testing.T) { + repo := createTestRepo(t) + defer repo.Free() + defer os.RemoveAll(repo.Workdir()) + + commitId1, _ := seedTestRepo(t, repo) + commitId2, _ := updateReadme(t, repo, "foo\nbar\nbaz\n") + + opts := BlameOptions{ + NewestCommit: commitId2, + OldestCommit: nil, + MinLine: 1, + MaxLine: 3, + } + blame, err := repo.BlameFile("README", &opts) + checkFatal(t, err) + defer blame.Free() + if blame.HunkCount() != 2 { + t.Errorf("got hunk count %d, want 2", blame.HunkCount()) + } + + hunk1, err := blame.HunkByIndex(0) + checkFatal(t, err) + checkHunk(t, hunk1, BlameHunk{ + LinesInHunk: 1, + FinalCommitId: commitId1, + FinalStartLineNumber: 1, + OrigCommitId: commitId1, + OrigPath: "README", + OrigStartLineNumber: 1, + Boundary: true, + }) + + hunk2, err := blame.HunkByIndex(1) + checkFatal(t, err) + checkHunk(t, hunk2, BlameHunk{ + LinesInHunk: 2, + FinalCommitId: commitId2, + FinalStartLineNumber: 2, + OrigCommitId: commitId2, + OrigPath: "README", + OrigStartLineNumber: 2, + Boundary: false, + }) +} + +func checkHunk(t *testing.T, hunk, want BlameHunk) { + hunk.FinalSignature = nil + want.FinalSignature = nil + hunk.OrigSignature = nil + want.OrigSignature = nil + if !reflect.DeepEqual(hunk, want) { + t.Fatalf("got hunk %+v, want %+v", hunk, want) + } +}